https://github.com/JacekKosciesza/StarWars
GraphQL 'Star Wars' example using GraphQL for .NET, ASP.NET Core, Entity Framework Core
https://github.com/JacekKosciesza/StarWars
asp-net-core dotnet-core entity-framework-core graphiql graphql graphql-dotnet star-wars
Last synced: about 1 month ago
JSON representation
GraphQL 'Star Wars' example using GraphQL for .NET, ASP.NET Core, Entity Framework Core
- Host: GitHub
- URL: https://github.com/JacekKosciesza/StarWars
- Owner: JacekKosciesza
- License: mit
- Archived: true
- Created: 2017-02-12T16:01:26.000Z (about 8 years ago)
- Default Branch: master
- Last Pushed: 2017-12-04T21:53:30.000Z (over 7 years ago)
- Last Synced: 2024-10-27T11:50:17.026Z (6 months ago)
- Topics: asp-net-core, dotnet-core, entity-framework-core, graphiql, graphql, graphql-dotnet, star-wars
- Language: C#
- Homepage:
- Size: 141 KB
- Stars: 622
- Watchers: 51
- Forks: 98
- Open Issues: 10
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-dotnet-core-applications - **StarWars** - is a GraphQL-based ASP.NET Core Star Wars application. (Sample & Reference Applications)
README
# GraphQL 'Star Wars' example using GraphQL for .NET, ASP.NET Core, Entity Framework Core
## Examples
* Basic - simple 'Hello GraphQL!' example based on console version from [GraphQL for .NET on GitHub](https://github.com/graphql-dotnet/graphql-dotnet),
but using ASP.NET Core, Entity Framework Core and some best practices, patterns and principles.* Advanced - GraphQL queries and mutations with full 'Star Wars' database (see [GraphQL Specification by Facebook](https://github.com/facebook/graphql) and [GraphQL.js - reference implementation](https://github.com/graphql/graphql-js))
## Roadmap
- [x] Basic
- [x] Simple tutorial (step/screenshot/code)
- [ ] Detailed tutorial (steps explanation)
- [x] 3-Layers (Api, Core, Data) architecture
- [x] DDD (Domain Driven Design) hexagonal architecture
- [x] Dependency Inversion (deafult ASP.NET Core IoC container)
- [x] GraphQL controller
- [x] In Memory 'Droid' Repository
- [x] Entity Framework 'Droid' Repository
- [x] Automatic database creation
- [x] Seed database data
- [x] EF Migrations
- [x] Graph*i*QL
- [x] Unit Tests
- [x] Visual Studio 2017 RC upgrade
- [x] Integration Tests
- [x] Logs
- [x] Code Coverage
- [x] Continous Integration
- [ ] Advanced
- [x] Full 'Star Wars' database (Episodes, Characters, Planets, Humans etc.)
- [x] Base/generic repository
- [x] Visual Studio 2017 RTM upgrade
- [x] Repositories
- [ ] GraphQL queries
- [ ] GraphQL mutations
- [ ] Docker
- [ ] PWA (Progressive Web App)
- [ ] Identity microservice
- [ ] Angular frontend
- [ ] Apollo GraphQL Client for Angular
- [ ] Service Worker
- [ ] IndexedDB
- ...## Tutorials
### Basic
* Create 'StarWars' empty solution
* Add 'ASP.NET Core Web Application (.NET Core)' project named 'StarWars.Api'
* Select Web API template
* Update all NuGet packages
* Update project.json with correct runtime
```json
"runtimes": {
"win10-x64": { }
}
```* Install GraphQL NuGet package
* Create 'StarWars.Core' project
* Create 'Droid' model
```csharp
namespace StarWars.Core.Models
{
public class Droid
{
public int Id { get; set; }
public string Name { get; set; }
}
}
```* Create 'DroidType' model
```csharp
using GraphQL.Types;
using StarWars.Core.Models;namespace StarWars.Api.Models
{
public class DroidType : ObjectGraphType
{
public DroidType()
{
Field(x => x.Id).Description("The Id of the Droid.");
Field(x => x.Name, nullable: true).Description("The name of the Droid.");
}
}
}
```* Create 'StarWarsQuery' model
```csharp
using GraphQL.Types;
using StarWars.Core.Models;namespace StarWars.Api.Models
{
public class StarWarsQuery : ObjectGraphType
{
public StarWarsQuery()
{
Field(
"hero",
resolve: context => new Droid { Id = 1, Name = "R2-D2" }
);
}
}
}
```* Create 'GraphQLQuery' model
```csharp
namespace StarWars.Api.Models
{
public class GraphQLQuery
{
public string OperationName { get; set; }
public string NamedQuery { get; set; }
public string Query { get; set; }
public string Variables { get; set; }
}
}
```* Create 'GraphQLController'
```csharp
using GraphQL;
using GraphQL.Types;
using Microsoft.AspNetCore.Mvc;
using StarWars.Api.Models;
using System.Threading.Tasks;namespace StarWars.Api.Controllers
{
[Route("graphql")]
public class GraphQLController : Controller
{
[HttpPost]
public async Task Post([FromBody] GraphQLQuery query)
{
var schema = new Schema { Query = new StarWarsQuery() };var result = await new DocumentExecuter().ExecuteAsync(_ =>
{
_.Schema = schema;
_.Query = query.Query;}).ConfigureAwait(false);
if (result.Errors?.Count > 0)
{
return BadRequest();
}return Ok(result);
}
}
}
```* Test using Postman
* Create 'IDroidRepository' interface
```csharp
using StarWars.Core.Models;
using System.Threading.Tasks;namespace StarWars.Core.Data
{
public interface IDroidRepository
{
Task Get(int id);
}
}
```
* Create 'StarWars.Data' project
* Create in memory 'DroidRepository'
```csharp
using StarWars.Core.Data;
using System.Collections.Generic;
using System.Threading.Tasks;
using StarWars.Core.Models;
using System.Linq;namespace StarWars.Data.InMemory
{
public class DroidRepository : IDroidRepository
{
private List _droids = new List {
new Droid { Id = 1, Name = "R2-D2" }
};public Task Get(int id)
{
return Task.FromResult(_droids.FirstOrDefault(droid => droid.Id == id));
}
}
}
```* Use 'IDroidRepository' in StarWarsQuery
```csharp
using GraphQL.Types;
using StarWars.Core.Data;namespace StarWars.Api.Models
{
public class StarWarsQuery : ObjectGraphType
{
private IDroidRepository _droidRepository { get; set; }public StarWarsQuery(IDroidRepository _droidRepository)
{
Field(
"hero",
resolve: context => _droidRepository.Get(1)
);
}
}
}
```* Update creation of StarWarsQuery in GraphQLController
```csharp
// ...
public async Task Post([FromBody] GraphQLQuery query)
{
var schema = new Schema { Query = new StarWarsQuery(new DroidRepository()) };
// ...
```* Test using Postman
* Configure dependency injection in Startup.cs
```csharp
// ...
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();services.AddTransient();
services.AddTransient();
}
// ...
```* Use constructor injection of StarWarsQuery in GraphQLController
```csharp
// ...
public class GraphQLController : Controller
{
private StarWarsQuery _starWarsQuery { get; set; }public GraphQLController(StarWarsQuery starWarsQuery)
{
_starWarsQuery = starWarsQuery;
}[HttpPost]
public async Task Post([FromBody] GraphQLQuery query)
{
var schema = new Schema { Query = _starWarsQuery };
// ...
```* Add Microsoft.EntityFrameworkCore and Microsoft.EntityFrameworkCore.SqlServer Nuget packages to StarWars.Data project

* Create StarWarsContext
```csharp
using Microsoft.EntityFrameworkCore;
using StarWars.Core.Models;namespace StarWars.Data.EntityFramework
{
public class StarWarsContext : DbContext
{
public StarWarsContext(DbContextOptions options)
: base(options)
{
Database.EnsureCreated();
}public DbSet Droids { get; set; }
}
}
```* Update 'appsetting.json' with database connection
```json
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"ConnectionStrings": {
"StarWarsDatabaseConnection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=StarWars;Integrated Security=SSPI;integrated security=true;MultipleActiveResultSets=True;"
}
}
```* Create EF droid repository
```csharp
using StarWars.Core.Data;
using System.Threading.Tasks;
using StarWars.Core.Models;
using Microsoft.EntityFrameworkCore;namespace StarWars.Data.EntityFramework.Repositories
{
public class DroidRepository : IDroidRepository
{
private StarWarsContext _db { get; set; }public DroidRepository(StarWarsContext db)
{
_db = db;
}public Task Get(int id)
{
return _db.Droids.FirstOrDefaultAsync(droid => droid.Id == id);
}
}
}
```* Create seed data as an extension to StarWarsContext
```csharp
using StarWars.Core.Models;
using System.Linq;namespace StarWars.Data.EntityFramework.Seed
{
public static class StarWarsSeedData
{
public static void EnsureSeedData(this StarWarsContext db)
{
if (!db.Droids.Any())
{
var droid = new Droid
{
Name = "R2-D2"
};
db.Droids.Add(droid);
db.SaveChanges();
}
}
}
}
```* Configure dependency injection and run data seed in Startup.cs
```csharp
// ...
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();services.AddTransient();
services.AddTransient();
services.AddDbContext(options =>
options.UseSqlServer(Configuration["ConnectionStrings:StarWarsDatabaseConnection"])
);
}public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory, StarWarsContext db)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();app.UseMvc();
db.EnsureSeedData();
}
// ...
```
* Run application and make sure database is created
* Final test using Postman
#### Entity Framework Migrations
* Add 'Microsoft.EntityFrameworkCore.Design' NuGet package to 'StarWars.Data' project
* Add 'Microsoft.EntityFrameworkCore.Tools.DotNet' NuGet package to 'StarWars.Data' project
* Add tools section in project.json (StarWars.Data)
```json
"tools": {
"Microsoft.EntityFrameworkCore.Tools.DotNet": "1.1.0-preview4-final"
}
```
* Add official workaround for problems with targeting class library (Modify your class library to be a startup application)
* Add main entry point
```csharp
namespace StarWars.Data.EntityFramework.Workaround
{
// WORKAROUND: https://docs.efproject.net/en/latest/miscellaneous/cli/dotnet.html#targeting-class-library-projects-is-not-supported
public static class Program
{
public static void Main() { }
}
}
```
* Add build option in project.json
```json
"buildOptions": {
"emitEntryPoint": true
}
```
* Run migrations command from the console
```
dotnet ef migrations add Inital -o .\EntityFramework\Migrations
```
#### Grahp*i*QL
* Add NPM configuration file 'package.json' to StarWars.Api project
* Add GraphiQL dependencies and webpack bundle task
```json
{
"version": "1.0.0",
"name": "starwars-graphiql",
"private": true,
"scripts": {
"start": "webpack --progress"
},
"dependencies": {
"graphiql": "^0.7.8",
"graphql": "^0.7.0",
"isomorphic-fetch": "^2.1.1",
"react": "^15.3.1",
"react-dom": "^15.3.1"
},
"devDependencies": {
"babel": "^5.6.14",
"babel-loader": "^5.3.2",
"css-loader": "^0.24.0",
"extract-text-webpack-plugin": "^1.0.1",
"postcss-loader": "^0.10.1",
"style-loader": "^0.13.1",
"webpack": "^1.13.0"
}
}
```* Add webpack configuration 'webpack.config.js'
```javascript
var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin');var output = './wwwroot';
module.exports = {
entry: {
'bundle': './Scripts/app.js'
},output: {
path: output,
filename: '[name].js'
},resolve: {
extensions: ['', '.js', '.json']
},module: {
loaders: [
{ test: /\.js/, loader: 'babel', exclude: /node_modules/ },
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader') }
]
},plugins: [
new ExtractTextPlugin('style.css', { allChunks: true })
]
};
```* Install 'NPM Task Runner' extension
* Configure 'After Build' step in 'Task Runner Explorer'

```json
"-vs-binding": { "AfterBuild": [ "start" ] }
```* Add 'Get' action to GraphQL controller and GraphiQL view (~/Views/GraphQL/index.cshtml)
```csharp
// ...
public class GraphQLController : Controller
{
// ...
[HttpGet]
public IActionResult Index()
{
return View();
}
// ...
}
```
```html
GraphiQL
```
* Add GraphiQL scripts and styles (app.js and app.css to ~/GraphiQL)
* app.js
```javascript
import React from 'react';
import ReactDOM from 'react-dom';
import GraphiQL from 'graphiql';
import fetch from 'isomorphic-fetch';
import 'graphiql/graphiql.css';
import './app.css';function graphQLFetcher(graphQLParams) {
return fetch(window.location.origin + '/graphql', {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(graphQLParams)
}).then(response => response.json());
}ReactDOM.render(, document.getElementById('app'));
```
* app.css
```css
html, body {
height: 100%;
margin: 0;
overflow: hidden;
width: 100%;
}#app {
height: 100vh;
}
```* Add static files support
* Add 'Microsoft.AspNetCore.StaticFiles' NuGet
* Update configuration in 'Startup.cs'
```csharp
// ...
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory, StarWarsContext db)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();app.UseStaticFiles();
app.UseMvc();db.EnsureSeedData();
}
// ...
```
* Build project and check if bundles were created by webpack under ~/wwwroot
* Run project and enjoy Graph*i*QL
#### Unit Tests
* Create 'Class Library (.NET Core)' type 'StarWars.Tests.Unit' project
* Install 'xunit' NuGet package in StarWars.Tests.Unit project
* Install 'dotnet-test-xunit' NuGet package in StarWars.Tests.Unit project
* Make changes to project.json
* Set 'testRunner'
* Reference 'StarWars.Data' project
* Set 'runtimes'
```json
{
"version": "1.0.0-*",
"testRunner": "xunit",
"dependencies": {
"dotnet-test-xunit": "2.2.0-preview2-build1029",
"Microsoft.NETCore.App": "1.1.0",
"xunit": "2.1.0",
"StarWars.Data": {
"target": "project"
}
},"frameworks": {
"netcoreapp1.1": {
"imports": [
"dotnet5.6",
"portable-net45+win8"
]
}
},"runtimes": {
"win10-x64": {}
}
}
```* Create first test for in memory droid repository
```csharp
using StarWars.Data.InMemory;
using Xunit;namespace StarWars.Tests.Unit.Data.InMemory
{
public class DroidRepositoryShould
{
private readonly DroidRepository _droidRepository;
public DroidRepositoryShould()
{
// Given
_droidRepository = new DroidRepository();
}[Fact]
public async void ReturnR2D2DroidGivenIdOf1()
{
// When
var droid = await _droidRepository.Get(1);// Then
Assert.NotNull(droid);
Assert.Equal("WRONG_NAME", droid.Name);
}
}
}
```* Build and make sure that test is discovered by 'Test Explorer'
* Run test - it should fail (we want to make sure that we are testing the right thing)
* Fix test
```csharp
// ...
[Fact]
public async void ReturnR2D2DroidGivenIdOf1()
{
// When
var droid = await _droidRepository.Get(1);// Then
Assert.NotNull(droid);
Assert.Equal("R2-D2", droid.Name);
}
// ...
```
* Run test again - it should pass
* Install 'Moq' NuGet package
* Install 'Microsoft.EntityFrameworkCore.InMemory' NuGet package
* Add reference to 'StarWars.Core' in project.json
```json
{
"dependencies": {
"StarWars.Core": {
"target": "project"
}
}
}
```* Create EF droid repository unit test
```csharp
using Microsoft.EntityFrameworkCore;
using StarWars.Core.Models;
using StarWars.Data.EntityFramework;
using StarWars.Data.EntityFramework.Repositories;
using Xunit;namespace StarWars.Tests.Unit.Data.EntityFramework.Repositories
{
public class DroidRepositoryShould
{
private readonly DroidRepository _droidRepository;
public DroidRepositoryShould()
{
// Given
// https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/in-memory
var options = new DbContextOptionsBuilder()
.UseInMemoryDatabase(databaseName: "StarWars")
.Options;
using (var context = new StarWarsContext(options))
{
context.Droids.Add(new Droid { Id = 1, Name = "R2-D2" });
context.SaveChanges();
}
var starWarsContext = new StarWarsContext(options);
_droidRepository = new DroidRepository(starWarsContext);
}[Fact]
public async void ReturnR2D2DroidGivenIdOf1()
{
// When
var droid = await _droidRepository.Get(1);// Then
Assert.NotNull(droid);
Assert.Equal("R2-D2", droid.Name);
}
}
}
```* Create GraphQLController unit test
* First refactor controller to be more testable by using constructor injection
```csharp
using GraphQL;
using GraphQL.Types;
using Microsoft.AspNetCore.Mvc;
using StarWars.Api.Models;
using System.Threading.Tasks;namespace StarWars.Api.Controllers
{
[Route("graphql")]
public class GraphQLController : Controller
{
private IDocumentExecuter _documentExecuter { get; set; }
private ISchema _schema { get; set; }public GraphQLController(IDocumentExecuter documentExecuter, ISchema schema)
{
_documentExecuter = documentExecuter;
_schema = schema;
}[HttpGet]
public IActionResult Index()
{
return View();
}[HttpPost]
public async Task Post([FromBody] GraphQLQuery query)
{
var executionOptions = new ExecutionOptions { Schema = _schema, Query = query.Query };
var result = await _documentExecuter.ExecuteAsync(executionOptions).ConfigureAwait(false);if (result.Errors?.Count > 0)
{
return BadRequest(result.Errors);
}return Ok(result);
}
}
}
```
* Configure dependency injection in 'Startup.cs'
```csharp
// ...
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddTransient();
var sp = services.BuildServiceProvider();
services.AddTransient(_ => new Schema { Query = sp.GetService() });
}
// ...
```
* Create test for 'Index' and 'Post' actions
```csharp
using GraphQL;
using GraphQL.Types;
using Microsoft.AspNetCore.Mvc;
using Moq;
using StarWars.Api.Controllers;
using StarWars.Api.Models;
using System.Threading.Tasks;
using Xunit;namespace StarWars.Tests.Unit.Api.Controllers
{
public class GraphQLControllerShould
{
private GraphQLController _graphqlController { get; set; }public GraphQLControllerShould()
{
// Given
var documentExecutor = new Mock();
documentExecutor.Setup(x => x.ExecuteAsync(It.IsAny())).Returns(Task.FromResult(new ExecutionResult()));
var schema = new Mock();
_graphqlController = new GraphQLController(documentExecutor.Object, schema.Object);
}[Fact]
public void ReturnNotNullViewResult()
{
// When
var result = _graphqlController.Index() as ViewResult;// Then
Assert.NotNull(result);
Assert.IsType(result);
}[Fact]
public async void ReturnNotNullExecutionResult()
{
// Given
var query = new GraphQLQuery { Query = @"{ ""query"": ""query { hero { id name } }""" };// When
var result = await _graphqlController.Post(query);// Then
Assert.NotNull(result);
var okObjectResult = Assert.IsType(result);
var executionResult = okObjectResult.Value;
Assert.NotNull(executionResult);
}
}
}
```#### Visual Studio 2017 RC upgrade
* Open solution in VS 2017 and let the upgrade tool do the job
* Upgrade of 'StarWars.Tests.Unit' failed, so I had to remove all project dependencies and reload it
```json
{
"dependencies": {
// remove this:
"StarWars.Data": {
"target": "project"
},
"StarWars.Core": {
"target": "project"
}
// ...
}
}
```* Replace old test txplorer runner for the xUnit.net framework (dotnet-test-xunit) with new one (xunit.runner.visualstudio)
* Install (xunit.runner.visualstudio) dependency (Microsoft.DotNet.InternalAbstractions)
#### Integration Tests
* Create 'xUnit Test Project (.NET Core)' type 'StarWars.Tests.Integration' project
* Change target framework from 'netcoreapp1.0' to 'netcoreapp1.1'
```xml
netcoreapp1.1
```
* Install 'Microsoft.AspNetCore.TestHost' NuGet package
* Use EF in memory database for 'Test' evironment
* Install 'Microsoft.EntityFrameworkCore.InMemory' NuGet package
* Configure it in 'Startup.cs'
```csharp
// ...
private IHostingEnvironment Env { get; set; }public class Startup
{
// ...
Env = env;
}public void ConfigureServices(IServiceCollection services)
{
// ...
if (Env.IsEnvironment("Test"))
{
services.AddDbContext(options =>
options.UseInMemoryDatabase(databaseName: "StarWars"));
}
else
{
services.AddDbContext(options =>
options.UseSqlServer(Configuration["ConnectionStrings:StarWarsDatabaseConnection"]));
}
// ...
}
// ...
```
* Create integration test for GraphQL query (POST)
```csharp
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using StarWars.Api;
using System.Net.Http;
using System.Text;
using Xunit;namespace StarWars.Tests.Integration.Api.Controllers
{
public class GraphQLControllerShould
{
private readonly TestServer _server;
private readonly HttpClient _client;public GraphQLControllerShould()
{
_server = new TestServer(new WebHostBuilder()
.UseEnvironment("Test")
.UseStartup()
);
_client = _server.CreateClient();
}[Fact]
public async void ReturnR2D2Droid()
{
// Given
var query = @"{
""query"": ""query { hero { id name } }""
}";
var content = new StringContent(query, Encoding.UTF8, "application/json");// When
var response = await _client.PostAsync("/graphql", content);// Then
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("R2-D2", responseString);
}
}
}
```
#### Logs* Make sure that logger is configured in Startup.cs
```csharp
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory, StarWarsContext db)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
// ...
}
```* Override ToString method of GraphQLQuery class
```csharp
public override string ToString()
{
var builder = new StringBuilder();
builder.AppendLine();
if (!string.IsNullOrWhiteSpace(OperationName))
{
builder.AppendLine($"OperationName = {OperationName}");
}
if (!string.IsNullOrWhiteSpace(NamedQuery))
{
builder.AppendLine($"NamedQuery = {NamedQuery}");
}
if (!string.IsNullOrWhiteSpace(Query))
{
builder.AppendLine($"Query = {Query}");
}
if (!string.IsNullOrWhiteSpace(Variables))
{
builder.AppendLine($"Variables = {Variables}");
}return builder.ToString();
}
```
* Add logger to GraphQLController
```csharp
public class GraphQLController : Controller
{
// ...
private readonly ILogger _logger;public GraphQLController(IDocumentExecuter documentExecuter, ISchema schema, ILogger logger)
{
// ...
_logger = logger;
}[HttpGet]
public IActionResult Index()
{
_logger.LogInformation("Got request for GraphiQL. Sending GUI back");
return View();
}[HttpPost]
public async Task Post([FromBody] GraphQLQuery query)
{
// ...
if (result.Errors?.Count > 0)
{
_logger.LogError("GraphQL errors: {0}", result.Errors);
return BadRequest(result);
}_logger.LogDebug("GraphQL execution result: {result}", JsonConvert.SerializeObject(result.Data));
return Ok(result);
}
}
```* Add logger to DroidRepository
```csharp
namespace StarWars.Data.EntityFramework.Repositories
{
public class DroidRepository : IDroidRepository
{
private StarWarsContext _db { get; set; }
private readonly ILogger _logger;public DroidRepository(StarWarsContext db, ILogger logger)
{
_db = db;
_logger = logger;
}public Task Get(int id)
{
_logger.LogInformation("Get droid with id = {id}", id);
return _db.Droids.FirstOrDefaultAsync(droid => droid.Id == id);
}
}
}
```* Add logger to StarWarsContext
```csharp
namespace StarWars.Data.EntityFramework
{
public class StarWarsContext : DbContext
{
public readonly ILogger _logger;public StarWarsContext(DbContextOptions options, ILogger logger)
: base(options)
{
_logger = logger;
// ...
}
}
}
```* Add logger to StarWarsSeedData
```csharp
namespace StarWars.Data.EntityFramework.Seed
{
public static class StarWarsSeedData
{
public static void EnsureSeedData(this StarWarsContext db)
{
db._logger.LogInformation("Seeding database");
if (!db.Droids.Any())
{
db._logger.LogInformation("Seeding droids");
// ...
}
}
}
}
```* Fix controller unit test
```csharp
public class GraphQLControllerShould
{
public GraphQLControllerShould()
{
// ...
var logger = new Mock>();
_graphqlController = new GraphQLController(documentExecutor.Object, schema.Object, logger.Object);
}
// ...
}
```* Fix repository unit test
```csharp
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using StarWars.Core.Models;
using StarWars.Data.EntityFramework;
using StarWars.Data.EntityFramework.Repositories;
using Xunit;namespace StarWars.Tests.Unit.Data.EntityFramework.Repositories
{
public class DroidRepositoryShould
{
public DroidRepositoryShould()
{
var dbLogger = new Mock>();
// ...
using (var context = new StarWarsContext(options, dbLogger.Object))
{
// ...
}
// ...
var repoLogger = new Mock>();
_droidRepository = new DroidRepository(starWarsContext, repoLogger.Object);
}
}
}
```* Enjoy console logs

#### Code Coverage
* Install OpenCover NuGet package
* Add path to OpenCover tools to 'Path' environment variable. In my case it was:
```
C:\Users\Jacek_Kosciesza\.nuget\packages\opencover\4.6.519\tools\
```* Set 'Full' debug type in all projects (StarWars.Api.csproj, StarWars.Core.csproj, StarWars.Data.csproj). This is needed to produce *.pdb files which are understandable by OpenCover.
```xml
Full
```
* Run OpenCover in the console
```
OpenCover.Console.exe
-target:"dotnet.exe"
-targetargs:"test -f netcoreapp1.1 -c Release Tests/StarWars.Tests.Unit/StarWars.Tests.Unit.csproj"
-hideskipped:File
-output:coverage/unit/coverage.xml
-oldStyle
-filter:"+[StarWars*]* -[StarWars.Tests*]* -[StarWars.Api]*Program -[StarWars.Api]*Startup -[StarWars.Data]*EntityFramework.Workaround.Program -[StarWars.Data]*EntityFramework.Migrations* -[StarWars.Data]*EntityFramework.Seed*"
-searchdirs:"Tests/StarWars.Tests.Unit/bin/Release/netcoreapp1.1"
-register:user
```
* Install 'ReportGenerator' NuGet package
* Create simple script (unit-tests.bat)
```
mkdir coverage\unit
OpenCover.Console.exe -target:"dotnet.exe" -targetargs:"test -f netcoreapp1.1 -c Release Tests/StarWars.Tests.Unit/StarWars.Tests.Unit.csproj" -hideskipped:File -output:coverage/unit/coverage.xml -oldStyle -filter:"+[StarWars*]* -[StarWars.Tests*]* -[StarWars.Api]*Program -[StarWars.Api]*Startup -[StarWars.Data]*EntityFramework.Workaround.Program -[StarWars.Data]*EntityFramework.Migrations* -[StarWars.Data]*EntityFramework.Seed*" -searchdirs:"Tests/StarWars.Tests.Unit/bin/Release/netcoreapp1.1" -register:user
ReportGenerator.exe -reports:coverage/unit/coverage.xml -targetdir:coverage/unit -verbosity:Error
start .\coverage\unit\index.htm
```* Enjoy HTML based code coverage report

#### Continous Integration* Create new project in VSTS (Visual Studio Team Services)
* Create new build definition "ASP.NET Core Preview". Select GitHub, Hosted VS2017 default agent queue and continous integration. ~~At the moment hosted agents don't support *.csproj based .NET Core projects, so we have to wait for a while, see this issue: [Support for .NET Core .csproj files? #3311](https://github.com/Microsoft/vsts-tasks/issues/3311)~~

* Add new GitHub service connection
* Setup repository
* Switch to "New Build Editor"
* Setup build process (tasks, build steps)
* Setup projects in Test build step
```
**/Tests/StarWars.Tests.Unit/StarWars.Tests.Unit.csproj;**/Tests/StarWars.Tests.Integration/StarWars.Tests.Integration.csproj
```
* Queue build. Make sure it succeeded and executed unit and integration tests.

* Enable build badge (after save you will see link to build status image).
### Advanced
#### Full 'Star Wars' database (see [Facebook GraphQL](https://github.com/facebook/graphql) and [GraphQL.js](https://github.com/graphql/graphql-js))
* Create models
```csharp
namespace StarWars.Core.Models
{
public class Episode
{
public int Id { get; set; }
public string Title { get; set; }
public virtual ICollection CharacterEpisodes { get; set; }
}
}
```
```csharp
namespace StarWars.Core.Models
{
public class Planet
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection Humans { get; set; }
}
}
```
```csharp
namespace StarWars.Core.Models
{
public class Character
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection CharacterEpisodes { get; set; }
public virtual ICollection CharacterFriends { get; set; }
public virtual ICollection FriendCharacters { get; set; }
}
}
```
```csharp
namespace StarWars.Core.Models
{
public class CharacterEpisode
{
public int CharacterId { get; set; }
public Character Character { get; set; }public int EpisodeId { get; set; }
public Episode Episode { get; set; }
}
}
```
```csharp
namespace StarWars.Core.Models
{
public class CharacterFriend
{
public int CharacterId { get; set; }
public Character Character { get; set; }public int FriendId { get; set; }
public Character Friend { get; set; }
}
}
```
```csharp
namespace StarWars.Core.Models
{
public class Droid : Character
{
public string PrimaryFunction { get; set; }
}
}
```
```csharp
namespace StarWars.Core.Models
{
public class Human : Character
{
public Planet HomePlanet { get; set; }
}
}
```* Update StarWarsContext
```csharp
namespace StarWars.Data.EntityFramework
{
public class StarWarsContext : DbContext
{
// ...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// https://docs.microsoft.com/en-us/ef/core/modeling/relationships
// http://stackoverflow.com/questions/38520695/multiple-relationships-to-the-same-table-in-ef7core// episodes
modelBuilder.Entity().HasKey(c => c.Id);
modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever();// planets
modelBuilder.Entity().HasKey(c => c.Id);
modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever();// characters
modelBuilder.Entity().HasKey(c => c.Id);
modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever();// characters-friends
modelBuilder.Entity().HasKey(t => new { t.CharacterId, t.FriendId});modelBuilder.Entity()
.HasOne(cf => cf.Character)
.WithMany(c => c.CharacterFriends)
.HasForeignKey(cf => cf.CharacterId)
.OnDelete(DeleteBehavior.Restrict);modelBuilder.Entity()
.HasOne(cf => cf.Friend)
.WithMany(t => t.FriendCharacters)
.HasForeignKey(cf => cf.FriendId)
.OnDelete(DeleteBehavior.Restrict);// characters-episodes
modelBuilder.Entity().HasKey(t => new { t.CharacterId, t.EpisodeId });modelBuilder.Entity()
.HasOne(cf => cf.Character)
.WithMany(c => c.CharacterEpisodes)
.HasForeignKey(cf => cf.CharacterId)
.OnDelete(DeleteBehavior.Restrict);modelBuilder.Entity()
.HasOne(cf => cf.Episode)
.WithMany(t => t.CharacterEpisodes)
.HasForeignKey(cf => cf.EpisodeId)
.OnDelete(DeleteBehavior.Restrict);// humans
modelBuilder.Entity().HasOne(h => h.HomePlanet).WithMany(p => p.Humans);
}public virtual DbSet Episodes { get; set; }
public virtual DbSet Planets { get; set; }
public virtual DbSet Characters { get; set; }
public virtual DbSet CharacterFriends { get; set; }
public virtual DbSet CharacterEpisodes { get; set; }
public virtual DbSet Droids { get; set; }
public virtual DbSet Humans { get; set; }
}
}
```* Update database seed data
```csharp
namespace StarWars.Data.EntityFramework.Seed
{
public static class StarWarsSeedData
{
public static void EnsureSeedData(this StarWarsContext db)
{
db._logger.LogInformation("Seeding database");// episodes
var newhope = new Episode { Id = 4, Title = "NEWHOPE" };
var empire = new Episode { Id = 5, Title = "EMPIRE" };
var jedi = new Episode { Id = 6, Title = "JEDI" };
var episodes = new List
{
newhope,
empire,
jedi,
};
if (!db.Episodes.Any())
{
db._logger.LogInformation("Seeding episodes");
db.Episodes.AddRange(episodes);
db.SaveChanges();
}// planets
var tatooine = new Planet { Id = 1, Name = "Tatooine" };
var alderaan = new Planet { Id = 2, Name = "Alderaan" };
var planets = new List
{
tatooine,
alderaan
};
if (!db.Planets.Any())
{
db._logger.LogInformation("Seeding planets");
db.Planets.AddRange(planets);
db.SaveChanges();
}// humans
var luke = new Human
{
Id = 1000,
Name = "Luke Skywalker",
CharacterEpisodes = new List
{
new CharacterEpisode { Episode = newhope },
new CharacterEpisode { Episode = empire },
new CharacterEpisode { Episode = jedi }
},
HomePlanet = tatooine
};
var vader = new Human
{
Id = 1001,
Name = "Darth Vader",
CharacterEpisodes = new List
{
new CharacterEpisode { Episode = newhope },
new CharacterEpisode { Episode = empire },
new CharacterEpisode { Episode = jedi }
},
HomePlanet = tatooine
};
var han = new Human
{
Id = 1002,
Name = "Han Solo",
CharacterEpisodes = new List
{
new CharacterEpisode { Episode = newhope },
new CharacterEpisode { Episode = empire },
new CharacterEpisode { Episode = jedi }
},
HomePlanet = tatooine
};
var leia = new Human
{
Id = 1003,
Name = "Leia Organa",
CharacterEpisodes = new List
{
new CharacterEpisode { Episode = newhope },
new CharacterEpisode { Episode = empire },
new CharacterEpisode { Episode = jedi }
},
HomePlanet = alderaan
};
var tarkin = new Human
{
Id = 1004,
Name = "Wilhuff Tarkin",
CharacterEpisodes = new List
{
new CharacterEpisode { Episode = newhope }
},
};
var humans = new List
{
luke,
vader,
han,
leia,
tarkin
};
if (!db.Humans.Any())
{
db._logger.LogInformation("Seeding humans");
db.Humans.AddRange(humans);
db.SaveChanges();
}// droids
var threepio = new Droid
{
Id = 2000,
Name = "C-3PO",
CharacterEpisodes = new List
{
new CharacterEpisode { Episode = newhope },
new CharacterEpisode { Episode = empire },
new CharacterEpisode { Episode = jedi }
},
PrimaryFunction = "Protocol"
};
var artoo = new Droid
{
Id = 2001,
Name = "R2-D2",
CharacterEpisodes = new List
{
new CharacterEpisode { Episode = newhope },
new CharacterEpisode { Episode = empire },
new CharacterEpisode { Episode = jedi }
},
PrimaryFunction = "Astromech"
};
var droids = new List
{
threepio,
artoo
};
if (!db.Droids.Any())
{
db._logger.LogInformation("Seeding droids");
db.Droids.AddRange(droids);
db.SaveChanges();
}// update character's friends
luke.CharacterFriends = new List
{
new CharacterFriend { Friend = han },
new CharacterFriend { Friend = leia },
new CharacterFriend { Friend = threepio },
new CharacterFriend { Friend = artoo }
};
vader.CharacterFriends = new List
{
new CharacterFriend { Friend = tarkin }
};
han.CharacterFriends = new List
{
new CharacterFriend { Friend = luke },
new CharacterFriend { Friend = leia },
new CharacterFriend { Friend = artoo }
};
leia.CharacterFriends = new List
{
new CharacterFriend { Friend = luke },
new CharacterFriend { Friend = han },
new CharacterFriend { Friend = threepio },
new CharacterFriend { Friend = artoo }
};
tarkin.CharacterFriends = new List
{
new CharacterFriend { Friend = vader }
};
threepio.CharacterFriends = new List
{
new CharacterFriend { Friend = luke },
new CharacterFriend { Friend = han },
new CharacterFriend { Friend = leia },
new CharacterFriend { Friend = artoo }
};
artoo.CharacterFriends = new List
{
new CharacterFriend { Friend = luke },
new CharacterFriend { Friend = han },
new CharacterFriend { Friend = leia }
};
var characters = new List
{
luke,
vader,
han,
leia,
tarkin,
threepio,
artoo
};
if (!db.CharacterFriends.Any())
{
db._logger.LogInformation("Seeding character's friends");
db.Characters.UpdateRange(characters);
db.SaveChanges();
}
}
}
}
```* Add 'Microsoft.EntityFrameworkCore.Tools' NuGet
* Set 'StarWars.Data' as a StartUp project
* Add 'Full' migrations
* Update database

* Set 'StarWars.Api' as a StartUp project
* Run 'StarWars.Api' to seed database

* Create integration test checking EF configuration and seeded data
```csharp
namespace StarWars.Tests.Integration.Data.EntityFramework
{
public class StarWarsContextShould
{
[Fact]
public async void ReturnR2D2Droid()
{
// Given
using (var db = new StarWarsContext())
{
// When
var r2d2 = await db.Droids
.Include("CharacterEpisodes.Episode")
.Include("CharacterFriends.Friend")
.FirstOrDefaultAsync(d => d.Id == 2001);// Then
Assert.NotNull(r2d2);
Assert.Equal("R2-D2", r2d2.Name);
Assert.Equal("Astromech", r2d2.PrimaryFunction);
var episodes = r2d2.CharacterEpisodes.Select(e => e.Episode.Title);
Assert.Equal(new string[] { "NEWHOPE", "EMPIRE", "JEDI" }, episodes);
var friends = r2d2.CharacterFriends.Select(e => e.Friend.Name);
Assert.Equal(new string[] { "Luke Skywalker", "Han Solo", "Leia Organa" }, friends);
}
}
}
}
```* Make sure all tests pass
* Update StarWarsQuery with new hero ("R2-D2") ID (2001)
```csharp
namespace StarWars.Api.Models
{
public class StarWarsQuery : ObjectGraphType
{
// ...
public StarWarsQuery(IDroidRepository _droidRepository)
{
Field(
"hero",
resolve: context => _droidRepository.Get(2001)
);
}
}
}
```* Make sure application still works
#### Base/generic repository
* Create generic entity interface
```csharp
namespace StarWars.Core.Data
{
public interface IEntity
{
TKey Id { get; set; }
}
}
```* Update models to inherit from IEntity interface (integer based id)
```csharp
namespace StarWars.Core.Models
{
public class Character : IEntity
{
// ...
}
}
```
```csharp
namespace StarWars.Core.Models
{
public class Episode : IEntity
{
// ...
}
}
```
```csharp
namespace StarWars.Core.Models
{
public class Planet : IEntity
{
// ...
}
}
```* Create base/generic repository interface
```csharp
namespace StarWars.Core.Data
{
public interface IBaseRepository
where TEntity : class
{
Task> GetAll();
Task Get(TKey id);
TEntity Add(TEntity entity);
void AddRange(IEnumerable entities);
void Delete(TKey id);
void Update(TEntity entity);
Task SaveChangesAsync();
}
}
```
* Create Entity Framework base/generic repository
```csharp
namespace StarWars.Data.EntityFramework.Repositories
{
public abstract class BaseRepository : IBaseRepository
where TEntity : class, IEntity, new()
{
protected DbContext _db;
protected readonly ILogger _logger;protected BaseRepository() { }
protected BaseRepository(DbContext db, ILogger logger)
{
_db = db;
_logger = logger;
}public virtual Task> GetAll()
{
return _db.Set().ToListAsync();
}public virtual Task Get(TKey id)
{
_logger.LogInformation("Get {type} with id = {id}", typeof(TEntity).Name, id);
return _db.Set().SingleOrDefaultAsync(c => c.Id.Equals(id));
}public virtual TEntity Add(TEntity entity)
{
_db.Set().Add(entity);
return entity;
}public void AddRange(IEnumerable entities)
{
_db.Set().AddRange(entities);
}public virtual void Delete(TKey id)
{
var entity = new TEntity { Id = id };
_db.Set().Attach(entity);
_db.Set().Remove(entity);
}public virtual async Task SaveChangesAsync()
{
return (await _db.SaveChangesAsync()) > 0;
}public virtual void Update(TEntity entity)
{
_db.Set().Attach(entity);
_db.Entry(entity).State = EntityState.Modified;
}
}
}
```* Refactor EF Droid repository
```csharp
namespace StarWars.Core.Data
{
public interface IDroidRepository : IBaseRepository { }
}
```
```csharp
namespace StarWars.Data.EntityFramework.Repositories
{
public class DroidRepository : BaseRepository, IDroidRepository
{
public DroidRepository() { }public DroidRepository(StarWarsContext db, ILogger logger)
: base(db, logger)
{
}
}
}
```
* Refactor in-memeory Droid repository
```csharp
namespace StarWars.Data.InMemory
{
public class DroidRepository : IDroidRepository
{
private readonly ILogger _logger;public DroidRepository() { }
public DroidRepository(ILogger logger)
{
_logger = logger;
}private List _droids = new List {
new Droid { Id = 1, Name = "R2-D2" }
};public Task Get(int id)
{
_logger.LogInformation("Get droid with id = {id}", id);
return Task.FromResult(_droids.FirstOrDefault(droid => droid.Id == id));
}// ...
// rest of the methods are not implemented
// for now they are just throwing NotImplementedException
}
}
```* Make sure tests and api stil works
#### Visual Studio 2017 RTM upgrade
* Update all NuGet packages for the solution (especially .NET Core v1.1.1)
* Use 'Package Manger Console' to fix problems with upgrading 'Microsoft.NETCore.App' from v1.1.0 to v.1.1.1 (for some reason Consolidate option does not work). Do upgrade for all projects.

```
Install-Package Microsoft.NETCore.App
```
* Fix 'DroidType' unit test (capitalization of field names)
```csharp
namespace StarWars.Tests.Unit.Api.Models
{
public class DroidTypeShould
{
[Fact]
public void HaveIdAndNameFields()
{
// When
var droidType = new DroidType();// Then
Assert.NotNull(droidType);
Assert.True(droidType.HasField("Id"));
Assert.True(droidType.HasField("Name"));
}
}
}
```#### Repositories
* Create rest of the repositories (Character, Episode, Human, Planet)
```csharp
namespace StarWars.Core.Data
{
public interface IHumanRepository : IBaseRepository { }
}
```
```csharp
namespace StarWars.Data.EntityFramework.Repositories
{
public class HumanRepository : BaseRepository, IHumanRepository
{
public HumanRepository() { }public HumanRepository(StarWarsContext db, ILogger logger)
: base(db, logger)
{
}
}
}
```* Update base repository with 'include' versions
```csharp
namespace StarWars.Core.Data
{
public interface IBaseRepository
where TEntity : class
{
// ...
Task> GetAll(string include);
Task> GetAll(IEnumerable includes);
// ...Task Get(TKey id, string include);
Task Get(TKey id, IEnumerable includes);
// ...
}
}
```
```csharp
namespace StarWars.Data.EntityFramework.Repositories
{
public abstract class BaseRepository : IBaseRepository
where TEntity : class, IEntity, new()
{
// ...
public Task> GetAll(string include)
{
_logger.LogInformation("Get all {type}s (including {include})", typeof(TEntity).Name, include);
return _db.Set().Include(include).ToListAsync();
}public Task> GetAll(IEnumerable includes)
{
_logger.LogInformation("Get all {type}s (including [{includes}])", typeof(TEntity).Name, string.Join(",", includes));
var query = _db.Set().AsQueryable();
query = includes.Aggregate(query, (current, include) => current.Include(include));
return query.ToListAsync();
}// ...
public Task Get(TKey id, string include)
{
_logger.LogInformation("Get {type} with id = {id} (including {include})", typeof(TEntity).Name, id, include);
return _db.Set().Include(include).SingleOrDefaultAsync(c => c.Id.Equals(id));
}public Task Get(TKey id, IEnumerable includes)
{
_logger.LogInformation("Get {type} with id = {id} (including [{include}])", typeof(TEntity).Name, id, string.Join(",", includes));
var query = _db.Set().AsQueryable();
query = includes.Aggregate(query, (current, include) => current.Include(include));
return query.SingleOrDefaultAsync(c => c.Id.Equals(id));
}// ...
}
}
```* Create repositories CRUD unit tests
```csharp
namespace StarWars.Tests.Unit.Data.EntityFramework.Repositories
{
public class HumanRepositoryShould
{
private readonly HumanRepository _humanRepository;
private DbContextOptions _options;
private Mock> _dbLogger;
public HumanRepositoryShould()
{
// Given
_dbLogger = new Mock>();
_options = new DbContextOptionsBuilder()
.UseInMemoryDatabase(databaseName: "StarWars_HumanRepositoryShould")
.Options;
using (var context = new StarWarsContext(_options, _dbLogger.Object))
{
context.EnsureSeedData();
}
var starWarsContext = new StarWarsContext(_options, _dbLogger.Object);
var repoLogger = new Mock>();
_humanRepository = new HumanRepository(starWarsContext, repoLogger.Object);
}[Fact]
public async void ReturnLukeGivenIdOf1000()
{
// When
var luke = await _humanRepository.Get(1000);// Then
Assert.NotNull(luke);
Assert.Equal("Luke Skywalker", luke.Name);
}[Fact]
public async void ReturnLukeFriendsAndEpisodes()
{
// When
var character = await _humanRepository.Get(1000, includes: new[] { "CharacterEpisodes.Episode", "CharacterFriends.Friend" });// Then
Assert.NotNull(character);
Assert.NotNull(character.CharacterEpisodes);
var episodes = character.CharacterEpisodes.Select(e => e.Episode.Title);
Assert.Equal(new[] { "NEWHOPE", "EMPIRE", "JEDI" }, episodes);
Assert.NotNull(character.CharacterFriends);
var friends = character.CharacterFriends.Select(e => e.Friend.Name);
Assert.Equal(new[] { "Han Solo", "Leia Organa", "C-3PO", "R2-D2" }, friends);
}[Fact]
public async void ReturnLukesHomePlanet()
{
// When
var luke = await _humanRepository.Get(1000, include: "HomePlanet");// Then
Assert.NotNull(luke);
Assert.NotNull(luke.HomePlanet);
Assert.Equal("Tatooine", luke.HomePlanet.Name);
}[Fact]
public async void AddNewHuman()
{
// Given
var human10101 = new Human { Id = 10101, Name = "Human10101" };// When
_humanRepository.Add(human10101);
var saved = await _humanRepository.SaveChangesAsync();// Then
Assert.True(saved);
using (var db = new StarWarsContext(_options, _dbLogger.Object))
{
var human = await db.Humans.FindAsync(10101);
Assert.NotNull(human);
Assert.Equal(10101, human.Id);
Assert.Equal("Human10101", human.Name);// Cleanup
db.Humans.Remove(human);
await db.SaveChangesAsync();
}
}[Fact]
public async void UpdateExistingHuman()
{
// Given
var vader = await _humanRepository.Get(1001);
vader.Name = "Human1001";// When
_humanRepository.Update(vader);
var saved = await _humanRepository.SaveChangesAsync();// Then
Assert.True(saved);
using (var db = new StarWarsContext(_options, _dbLogger.Object))
{
var human = await db.Humans.FindAsync(1001);
Assert.NotNull(human);
Assert.Equal(1001, human.Id);
Assert.Equal("Human1001", human.Name);// Cleanup
human.Name = "Darth Vader";
db.Humans.Update(human);
await db.SaveChangesAsync();
}
}[Fact]
public async void DeleteExistingHuman()
{
// Given
using (var db = new StarWarsContext(_options, _dbLogger.Object))
{
var human10102 = new Human { Id = 10102, Name = "Human10102" };
await db.Humans.AddAsync(human10102);
await db.SaveChangesAsync();
}// When
_humanRepository.Delete(10102);
var saved = await _humanRepository.SaveChangesAsync();// Then
Assert.True(saved);
using (var db = new StarWarsContext(_options, _dbLogger.Object))
{
var deletedHuman = await db.Humans.FindAsync(10101);
Assert.Null(deletedHuman);
}
}
}
}
```* Check test results

#### GraphQL queries
* TDD (Test First) integration tests for queries at [GraphQL Specification by Facebook](https://github.com/facebook/graphql#query-syntax)
```csharp
namespace StarWars.Tests.Integration.Api.Controllers
{
public class GraphQLControllerShould
{
// ...
[Fact]
[Trait("test", "integration")]
public async void ExecuteHeroNameQuery()
{
// Given
const string query = @"{
""query"":
""query HeroNameQuery {
hero {
name
}
}""
}";
var content = new StringContent(query, Encoding.UTF8, "application/json");// When
var response = await _client.PostAsync("/graphql", content);// Then
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.NotNull(responseString);
var jobj = JObject.Parse(responseString);
Assert.NotNull(jobj);
Assert.Equal("R2-D2", (string)jobj["data"]["hero"]["name"]);
}[Fact]
[Trait("test", "integration")]
public async void ExecuteHeroNameAndFriendsQuery()
{
// Given
const string query = @"{
""query"":
""query HeroNameAndFriendsQuery {
hero {
id
name
friends {
id
name
}
}
}""
}";
var content = new StringContent(query, Encoding.UTF8, "application/json");// When
var response = await _client.PostAsync("/graphql", content);// Then
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.NotNull(responseString);
var jobj = JObject.Parse(responseString);
Assert.NotNull(jobj);
Assert.Equal(3, ((JArray)jobj["data"]["hero"]["friends"]).Count);
Assert.Equal("Luke Skywalker", (string)jobj["data"]["hero"]["friends"][0]["name"]);
Assert.Equal("Han Solo", (string)jobj["data"]["hero"]["friends"][1]["name"]);
Assert.Equal("Leia Organa", (string)jobj["data"]["hero"]["friends"][2]["name"]);
}[Fact]
[Trait("test", "integration")]
public async void ExecuteNestedQuery()
{
// Given
const string query = @"{
""query"":
""query NestedQuery {
hero {
name
friends {
name
appearsIn
friends {
name
}
}
}
}""
}";
var content = new StringContent(query, Encoding.UTF8, "application/json");// When
var response = await _client.PostAsync("/graphql", content);// Then
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.NotNull(responseString);
var jobj = JObject.Parse(responseString);
Assert.NotNull(jobj);
var luke = jobj["data"]["hero"]["friends"][0];
var episodes = ((JArray) luke["appearsIn"]).Select(e => (string)e).ToArray();
Assert.Equal(new[] { "NEWHOPE", "EMPIRE", "JEDI" }, episodes);
Assert.Equal(4, ((JArray)luke["friends"]).Count);
Assert.Equal("Han Solo", (string)luke["friends"][0]["name"]);
Assert.Equal("Leia Organa", (string)luke["friends"][1]["name"]);
Assert.Equal("C-3PO", (string)luke["friends"][2]["name"]);
Assert.Equal("R2-D2", (string)luke["friends"][3]["name"]);
}[Fact]
[Trait("test", "integration")]
public async void ExecuteFetchLukeQuery()
{
// Given
const string query = @"{
""query"":
""query FetchLukeQuery {
human(id: ""1000"") {
name
}
}
}""
}";
var content = new StringContent(query, Encoding.UTF8, "application/json");// When
var response = await _client.PostAsync("/graphql", content);// Then
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.NotNull(responseString);
var jobj = JObject.Parse(responseString);
Assert.NotNull(jobj);
Assert.Equal("Luke Skywalker", (string)jobj["data"]["human"]["name"]);
}[Fact]
[Trait("test", "integration")]
public async void ExecuteFetchLukeAliased()
{
// Given
const string query = @"{
""query"":
""query FetchLukeAliased {
luke: human(id: ""1000"") {
name
}
}
}""
}";
var content = new StringContent(query, Encoding.UTF8, "application/json");// When
var response = await _client.PostAsync("/graphql", content);// Then
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.NotNull(responseString);
var jobj = JObject.Parse(responseString);
Assert.NotNull(jobj);
Assert.Equal("Luke Skywalker", (string)jobj["data"]["human"]["name"]);
}[Fact]
[Trait("test", "integration")]
public async void ExecuteFetchLukeAndLeiaAliased()
{
// Given
const string query = @"{
""query"":
""query FetchLukeAliased {
luke: human(id: ""1000"") {
name
}
leia: human(id: ""1003"") {
name
}
}
}""
}";
var content = new StringContent(query, Encoding.UTF8, "application/json");// When
var response = await _client.PostAsync("/graphql", content);// Then
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.NotNull(responseString);
var jobj = JObject.Parse(responseString);
Assert.NotNull(jobj);
Assert.Equal("Luke Skywalker", (string)jobj["data"]["luke"]["name"]);
Assert.Equal("Leia Organa", (string)jobj["data"]["leia"]["name"]);
}[Fact]
[Trait("test", "integration")]
public async void ExecuteDuplicateFields()
{
// Given
const string query = @"{
""query"":
""query DuplicateFields {
luke: human(id: ""1000"") {
name
homePlanet
}
leia: human(id: ""1003"") {
name
homePlanet
}
}
}""
}";
var content = new StringContent(query, Encoding.UTF8, "application/json");// When
var response = await _client.PostAsync("/graphql", content);// Then
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.NotNull(responseString);
var jobj = JObject.Parse(responseString);
Assert.NotNull(jobj);
Assert.Equal("Luke Skywalker", (string)jobj["data"]["luke"]["name"]);
Assert.Equal("Tatooine", (string)jobj["data"]["luke"]["homePlanet"]);
Assert.Equal("Leia Organa", (string)jobj["data"]["leia"]["name"]);
Assert.Equal("Alderaan", (string)jobj["data"]["leia"]["homePlanet"]);
}[Fact]
[Trait("test", "integration")]
public async void ExecuteUseFragment()
{
// Given
const string query = @"{
""query"":
""query UseFragment {
luke: human(id: ""1000"") {
...HumanFragment
}
leia: human(id: ""1003"") {
...HumanFragment
}
}fragment HumanFragment on Human {
name
homePlanet
}
}""
}";
var content = new StringContent(query, Encoding.UTF8, "application/json");// When
var response = await _client.PostAsync("/graphql", content);// Then
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.NotNull(responseString);
var jobj = JObject.Parse(responseString);
Assert.NotNull(jobj);
Assert.Equal("Luke Skywalker", (string)jobj["data"]["luke"]["name"]);
Assert.Equal("Tatooine", (string)jobj["data"]["luke"]["homePlanet"]);
Assert.Equal("Leia Organa", (string)jobj["data"]["leia"]["name"]);
Assert.Equal("Alderaan", (string)jobj["data"]["leia"]["homePlanet"]);
}[Fact]
[Trait("test", "integration")]
public async void ExecuteCheckTypeOfR2()
{
// Given
const string query = @"{
""query"":
""query CheckTypeOfR2 {
hero {
__typename
name
}
}
}""
}";
var content = new StringContent(query, Encoding.UTF8, "application/json");// When
var response = await _client.PostAsync("/graphql", content);// Then
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.NotNull(responseString);
var jobj = JObject.Parse(responseString);
Assert.NotNull(jobj);
Assert.Equal("Droid", (string)jobj["data"]["hero"]["__typename"]);
Assert.Equal("R2-D2", (string)jobj["data"]["hero"]["name"]);
}[Fact]
[Trait("test", "integration")]
public async void ExecuteCheckTypeOfLuke()
{
// Given
const string query = @"{
""query"":
""query CheckTypeOfLuke {
hero(episode: EMPIRE) {
__typename
name
}
}
}""
}";
var content = new StringContent(query, Encoding.UTF8, "application/json");// When
var response = await _client.PostAsync("/graphql", content);// Then
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.NotNull(responseString);
var jobj = JObject.Parse(responseString);
Assert.NotNull(jobj);
Assert.Equal("Human", (string)jobj["data"]["hero"]["__typename"]);
Assert.Equal("Luke Skywalker", (string)jobj["data"]["hero"]["name"]);
}
}
}
```
