Awesome
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, 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 and GraphQL.js - reference implementation)
Roadmap
- Basic
- Simple tutorial (step/screenshot/code)
- Detailed tutorial (steps explanation)
- 3-Layers (Api, Core, Data) architecture
- DDD (Domain Driven Design) hexagonal architecture
- Dependency Inversion (deafult ASP.NET Core IoC container)
- GraphQL controller
- In Memory 'Droid' Repository
- Entity Framework 'Droid' Repository
- Automatic database creation
- Seed database data
- EF Migrations
- GraphiQL
- Unit Tests
- Visual Studio 2017 RC upgrade
- Integration Tests
- Logs
- Code Coverage
- Continous Integration
- Advanced
- Full 'Star Wars' database (Episodes, Characters, Planets, Humans etc.)
- Base/generic repository
- Visual Studio 2017 RTM upgrade
- 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
"runtimes": {
"win10-x64": { }
}
-
Install GraphQL NuGet package
-
Create 'StarWars.Core' project
-
Create 'Droid' model
namespace StarWars.Core.Models
{
public class Droid
{
public int Id { get; set; }
public string Name { get; set; }
}
}
- Create 'DroidType' model
using GraphQL.Types;
using StarWars.Core.Models;
namespace StarWars.Api.Models
{
public class DroidType : ObjectGraphType<Droid>
{
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
using GraphQL.Types;
using StarWars.Core.Models;
namespace StarWars.Api.Models
{
public class StarWarsQuery : ObjectGraphType
{
public StarWarsQuery()
{
Field<DroidType>(
"hero",
resolve: context => new Droid { Id = 1, Name = "R2-D2" }
);
}
}
}
- Create 'GraphQLQuery' model
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'
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<IActionResult> 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
using StarWars.Core.Models;
using System.Threading.Tasks;
namespace StarWars.Core.Data
{
public interface IDroidRepository
{
Task<Droid> Get(int id);
}
}
-
Create 'StarWars.Data' project
-
Create in memory 'DroidRepository'
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<Droid> _droids = new List<Droid> {
new Droid { Id = 1, Name = "R2-D2" }
};
public Task<Droid> Get(int id)
{
return Task.FromResult(_droids.FirstOrDefault(droid => droid.Id == id));
}
}
}
- Use 'IDroidRepository' in StarWarsQuery
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<DroidType>(
"hero",
resolve: context => _droidRepository.Get(1)
);
}
}
}
- Update creation of StarWarsQuery in GraphQLController
// ...
public async Task<IActionResult> Post([FromBody] GraphQLQuery query)
{
var schema = new Schema { Query = new StarWarsQuery(new DroidRepository()) };
// ...
-
Test using Postman
-
Configure dependency injection in Startup.cs
// ...
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddTransient<StarWarsQuery>();
services.AddTransient<IDroidRepository, DroidRepository>();
}
// ...
- Use constructor injection of StarWarsQuery in GraphQLController
// ...
public class GraphQLController : Controller
{
private StarWarsQuery _starWarsQuery { get; set; }
public GraphQLController(StarWarsQuery starWarsQuery)
{
_starWarsQuery = starWarsQuery;
}
[HttpPost]
public async Task<IActionResult> 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
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<Droid> Droids { get; set; }
}
}
- Update 'appsetting.json' with database connection
{
"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
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<Droid> Get(int id)
{
return _db.Droids.FirstOrDefaultAsync(droid => droid.Id == id);
}
}
}
- Create seed data as an extension to StarWarsContext
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
// ...
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddTransient<StarWarsQuery>();
services.AddTransient<IDroidRepository, DroidRepository>();
services.AddDbContext<StarWarsContext>(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)
"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
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
"buildOptions": { "emitEntryPoint": true }
- Run migrations command from the console
dotnet ef migrations add Inital -o .\EntityFramework\Migrations
GrahpiQL
-
Add NPM configuration file 'package.json' to StarWars.Api project
-
Add GraphiQL dependencies and webpack bundle task
{
"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'
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'
"-vs-binding": { "AfterBuild": [ "start" ] }
- Add 'Get' action to GraphQL controller and GraphiQL view (~/Views/GraphQL/index.cshtml)
// ...
public class GraphQLController : Controller
{
// ...
[HttpGet]
public IActionResult Index()
{
return View();
}
// ...
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>GraphiQL</title>
<link rel="stylesheet" href="~/style.css" />
</head>
<body>
<div id="app"></div>
<script src="~/bundle.js" type="text/javascript"></script>
</body>
</html>
-
Add GraphiQL scripts and styles (app.js and app.css to ~/GraphiQL)
- app.js
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(<GraphiQL fetcher={graphQLFetcher}/>, document.getElementById('app'));
- app.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'
// ... 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 GraphiQL
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'
{
"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
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
// ...
[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
{
"dependencies": {
"StarWars.Core": {
"target": "project"
}
}
}
- Create EF droid repository unit test
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<StarWarsContext>()
.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
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<IActionResult> 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'
// ... public void ConfigureServices(IServiceCollection services) { // ... services.AddTransient<IDocumentExecuter, DocumentExecuter>(); var sp = services.BuildServiceProvider(); services.AddTransient<ISchema>(_ => new Schema { Query = sp.GetService<StarWarsQuery>() }); } // ...
- Create test for 'Index' and 'Post' actions
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<IDocumentExecuter>(); documentExecutor.Setup(x => x.ExecuteAsync(It.IsAny<ExecutionOptions>())).Returns(Task.FromResult(new ExecutionResult())); var schema = new Mock<ISchema>(); _graphqlController = new GraphQLController(documentExecutor.Object, schema.Object); } [Fact] public void ReturnNotNullViewResult() { // When var result = _graphqlController.Index() as ViewResult; // Then Assert.NotNull(result); Assert.IsType<ViewResult>(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<OkObjectResult>(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
{
"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'
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
</PropertyGroup>
<!--...-->
</Project>
-
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'
// ... private IHostingEnvironment Env { get; set; } public class Startup { // ... Env = env; } public void ConfigureServices(IServiceCollection services) { // ... if (Env.IsEnvironment("Test")) { services.AddDbContext<StarWarsContext>(options => options.UseInMemoryDatabase(databaseName: "StarWars")); } else { services.AddDbContext<StarWarsContext>(options => options.UseSqlServer(Configuration["ConnectionStrings:StarWarsDatabaseConnection"])); } // ... } // ...
-
-
Create integration test for GraphQL query (POST)
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<Startup>()
);
_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
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory, StarWarsContext db)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
// ...
}
- Override ToString method of GraphQLQuery class
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
public class GraphQLController : Controller
{
// ...
private readonly ILogger _logger;
public GraphQLController(IDocumentExecuter documentExecuter, ISchema schema, ILogger<GraphQLController> logger)
{
// ...
_logger = logger;
}
[HttpGet]
public IActionResult Index()
{
_logger.LogInformation("Got request for GraphiQL. Sending GUI back");
return View();
}
[HttpPost]
public async Task<IActionResult> 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
namespace StarWars.Data.EntityFramework.Repositories
{
public class DroidRepository : IDroidRepository
{
private StarWarsContext _db { get; set; }
private readonly ILogger _logger;
public DroidRepository(StarWarsContext db, ILogger<DroidRepository> logger)
{
_db = db;
_logger = logger;
}
public Task<Droid> Get(int id)
{
_logger.LogInformation("Get droid with id = {id}", id);
return _db.Droids.FirstOrDefaultAsync(droid => droid.Id == id);
}
}
}
- Add logger to StarWarsContext
namespace StarWars.Data.EntityFramework
{
public class StarWarsContext : DbContext
{
public readonly ILogger _logger;
public StarWarsContext(DbContextOptions options, ILogger<StarWarsContext> logger)
: base(options)
{
_logger = logger;
// ...
}
}
}
- Add logger to StarWarsSeedData
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
public class GraphQLControllerShould
{
public GraphQLControllerShould()
{
// ...
var logger = new Mock<ILogger<GraphQLController>>();
_graphqlController = new GraphQLController(documentExecutor.Object, schema.Object, logger.Object);
}
// ...
}
- Fix repository unit test
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<ILogger<StarWarsContext>>();
// ...
using (var context = new StarWarsContext(options, dbLogger.Object))
{
// ...
}
// ...
var repoLogger = new Mock<ILogger<DroidRepository>>();
_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.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<!--...-->
<DebugType>Full</DebugType>
</PropertyGroup>
<!--...-->
</Project>
- 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 -
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 and GraphQL.js)
- Create models
namespace StarWars.Core.Models
{
public class Episode
{
public int Id { get; set; }
public string Title { get; set; }
public virtual ICollection<CharacterEpisode> CharacterEpisodes { get; set; }
}
}
namespace StarWars.Core.Models
{
public class Planet
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Human> Humans { get; set; }
}
}
namespace StarWars.Core.Models
{
public class Character
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<CharacterEpisode> CharacterEpisodes { get; set; }
public virtual ICollection<CharacterFriend> CharacterFriends { get; set; }
public virtual ICollection<CharacterFriend> FriendCharacters { get; set; }
}
}
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; }
}
}
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; }
}
}
namespace StarWars.Core.Models
{
public class Droid : Character
{
public string PrimaryFunction { get; set; }
}
}
namespace StarWars.Core.Models
{
public class Human : Character
{
public Planet HomePlanet { get; set; }
}
}
- Update StarWarsContext
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<Episode>().HasKey(c => c.Id);
modelBuilder.Entity<Episode>().Property(e => e.Id).ValueGeneratedNever();
// planets
modelBuilder.Entity<Planet>().HasKey(c => c.Id);
modelBuilder.Entity<Planet>().Property(e => e.Id).ValueGeneratedNever();
// characters
modelBuilder.Entity<Character>().HasKey(c => c.Id);
modelBuilder.Entity<Character>().Property(e => e.Id).ValueGeneratedNever();
// characters-friends
modelBuilder.Entity<CharacterFriend>().HasKey(t => new { t.CharacterId, t.FriendId});
modelBuilder.Entity<CharacterFriend>()
.HasOne(cf => cf.Character)
.WithMany(c => c.CharacterFriends)
.HasForeignKey(cf => cf.CharacterId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<CharacterFriend>()
.HasOne(cf => cf.Friend)
.WithMany(t => t.FriendCharacters)
.HasForeignKey(cf => cf.FriendId)
.OnDelete(DeleteBehavior.Restrict);
// characters-episodes
modelBuilder.Entity<CharacterEpisode>().HasKey(t => new { t.CharacterId, t.EpisodeId });
modelBuilder.Entity<CharacterEpisode>()
.HasOne(cf => cf.Character)
.WithMany(c => c.CharacterEpisodes)
.HasForeignKey(cf => cf.CharacterId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<CharacterEpisode>()
.HasOne(cf => cf.Episode)
.WithMany(t => t.CharacterEpisodes)
.HasForeignKey(cf => cf.EpisodeId)
.OnDelete(DeleteBehavior.Restrict);
// humans
modelBuilder.Entity<Human>().HasOne(h => h.HomePlanet).WithMany(p => p.Humans);
}
public virtual DbSet<Episode> Episodes { get; set; }
public virtual DbSet<Planet> Planets { get; set; }
public virtual DbSet<Character> Characters { get; set; }
public virtual DbSet<CharacterFriend> CharacterFriends { get; set; }
public virtual DbSet<CharacterEpisode> CharacterEpisodes { get; set; }
public virtual DbSet<Droid> Droids { get; set; }
public virtual DbSet<Human> Humans { get; set; }
}
}
- Update database seed data
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<Episode>
{
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<Planet>
{
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<CharacterEpisode>
{
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<CharacterEpisode>
{
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<CharacterEpisode>
{
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<CharacterEpisode>
{
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<CharacterEpisode>
{
new CharacterEpisode { Episode = newhope }
},
};
var humans = new List<Human>
{
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<CharacterEpisode>
{
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<CharacterEpisode>
{
new CharacterEpisode { Episode = newhope },
new CharacterEpisode { Episode = empire },
new CharacterEpisode { Episode = jedi }
},
PrimaryFunction = "Astromech"
};
var droids = new List<Droid>
{
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<CharacterFriend>
{
new CharacterFriend { Friend = han },
new CharacterFriend { Friend = leia },
new CharacterFriend { Friend = threepio },
new CharacterFriend { Friend = artoo }
};
vader.CharacterFriends = new List<CharacterFriend>
{
new CharacterFriend { Friend = tarkin }
};
han.CharacterFriends = new List<CharacterFriend>
{
new CharacterFriend { Friend = luke },
new CharacterFriend { Friend = leia },
new CharacterFriend { Friend = artoo }
};
leia.CharacterFriends = new List<CharacterFriend>
{
new CharacterFriend { Friend = luke },
new CharacterFriend { Friend = han },
new CharacterFriend { Friend = threepio },
new CharacterFriend { Friend = artoo }
};
tarkin.CharacterFriends = new List<CharacterFriend>
{
new CharacterFriend { Friend = vader }
};
threepio.CharacterFriends = new List<CharacterFriend>
{
new CharacterFriend { Friend = luke },
new CharacterFriend { Friend = han },
new CharacterFriend { Friend = leia },
new CharacterFriend { Friend = artoo }
};
artoo.CharacterFriends = new List<CharacterFriend>
{
new CharacterFriend { Friend = luke },
new CharacterFriend { Friend = han },
new CharacterFriend { Friend = leia }
};
var characters = new List<Character>
{
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
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)
namespace StarWars.Api.Models
{
public class StarWarsQuery : ObjectGraphType
{
// ...
public StarWarsQuery(IDroidRepository _droidRepository)
{
Field<DroidType>(
"hero",
resolve: context => _droidRepository.Get(2001)
);
}
}
}
- Make sure application still works
Base/generic repository
- Create generic entity interface
namespace StarWars.Core.Data
{
public interface IEntity<TKey>
{
TKey Id { get; set; }
}
}
- Update models to inherit from IEntity interface (integer based id)
namespace StarWars.Core.Models
{
public class Character : IEntity<int>
{
// ...
}
}
namespace StarWars.Core.Models
{
public class Episode : IEntity<int>
{
// ...
}
}
namespace StarWars.Core.Models
{
public class Planet : IEntity<int>
{
// ...
}
}
- Create base/generic repository interface
namespace StarWars.Core.Data
{
public interface IBaseRepository<TEntity, in TKey>
where TEntity : class
{
Task<List<TEntity>> GetAll();
Task<TEntity> Get(TKey id);
TEntity Add(TEntity entity);
void AddRange(IEnumerable<TEntity> entities);
void Delete(TKey id);
void Update(TEntity entity);
Task<bool> SaveChangesAsync();
}
}
- Create Entity Framework base/generic repository
namespace StarWars.Data.EntityFramework.Repositories
{
public abstract class BaseRepository<TEntity, TKey> : IBaseRepository<TEntity, TKey>
where TEntity : class, IEntity<TKey>, new()
{
protected DbContext _db;
protected readonly ILogger _logger;
protected BaseRepository() { }
protected BaseRepository(DbContext db, ILogger logger)
{
_db = db;
_logger = logger;
}
public virtual Task<List<TEntity>> GetAll()
{
return _db.Set<TEntity>().ToListAsync();
}
public virtual Task<TEntity> Get(TKey id)
{
_logger.LogInformation("Get {type} with id = {id}", typeof(TEntity).Name, id);
return _db.Set<TEntity>().SingleOrDefaultAsync(c => c.Id.Equals(id));
}
public virtual TEntity Add(TEntity entity)
{
_db.Set<TEntity>().Add(entity);
return entity;
}
public void AddRange(IEnumerable<TEntity> entities)
{
_db.Set<TEntity>().AddRange(entities);
}
public virtual void Delete(TKey id)
{
var entity = new TEntity { Id = id };
_db.Set<TEntity>().Attach(entity);
_db.Set<TEntity>().Remove(entity);
}
public virtual async Task<bool> SaveChangesAsync()
{
return (await _db.SaveChangesAsync()) > 0;
}
public virtual void Update(TEntity entity)
{
_db.Set<TEntity>().Attach(entity);
_db.Entry(entity).State = EntityState.Modified;
}
}
}
- Refactor EF Droid repository
namespace StarWars.Core.Data
{
public interface IDroidRepository : IBaseRepository<Droid, int> { }
}
namespace StarWars.Data.EntityFramework.Repositories
{
public class DroidRepository : BaseRepository<Droid, int>, IDroidRepository
{
public DroidRepository() { }
public DroidRepository(StarWarsContext db, ILogger<DroidRepository> logger)
: base(db, logger)
{
}
}
}
- Refactor in-memeory Droid repository
namespace StarWars.Data.InMemory
{
public class DroidRepository : IDroidRepository
{
private readonly ILogger _logger;
public DroidRepository() { }
public DroidRepository(ILogger<DroidRepository> logger)
{
_logger = logger;
}
private List<Droid> _droids = new List<Droid> {
new Droid { Id = 1, Name = "R2-D2" }
};
public Task<Droid> 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)
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)
namespace StarWars.Core.Data
{
public interface IHumanRepository : IBaseRepository<Human, int> { }
}
namespace StarWars.Data.EntityFramework.Repositories
{
public class HumanRepository : BaseRepository<Human, int>, IHumanRepository
{
public HumanRepository() { }
public HumanRepository(StarWarsContext db, ILogger<HumanRepository> logger)
: base(db, logger)
{
}
}
}
- Update base repository with 'include' versions
namespace StarWars.Core.Data
{
public interface IBaseRepository<TEntity, in TKey>
where TEntity : class
{
// ...
Task<List<TEntity>> GetAll(string include);
Task<List<TEntity>> GetAll(IEnumerable<string> includes);
// ...
Task<TEntity> Get(TKey id, string include);
Task<TEntity> Get(TKey id, IEnumerable<string> includes);
// ...
}
}
namespace StarWars.Data.EntityFramework.Repositories
{
public abstract class BaseRepository<TEntity, TKey> : IBaseRepository<TEntity, TKey>
where TEntity : class, IEntity<TKey>, new()
{
// ...
public Task<List<TEntity>> GetAll(string include)
{
_logger.LogInformation("Get all {type}s (including {include})", typeof(TEntity).Name, include);
return _db.Set<TEntity>().Include(include).ToListAsync();
}
public Task<List<TEntity>> GetAll(IEnumerable<string> includes)
{
_logger.LogInformation("Get all {type}s (including [{includes}])", typeof(TEntity).Name, string.Join(",", includes));
var query = _db.Set<TEntity>().AsQueryable();
query = includes.Aggregate(query, (current, include) => current.Include(include));
return query.ToListAsync();
}
// ...
public Task<TEntity> Get(TKey id, string include)
{
_logger.LogInformation("Get {type} with id = {id} (including {include})", typeof(TEntity).Name, id, include);
return _db.Set<TEntity>().Include(include).SingleOrDefaultAsync(c => c.Id.Equals(id));
}
public Task<TEntity> Get(TKey id, IEnumerable<string> includes)
{
_logger.LogInformation("Get {type} with id = {id} (including [{include}])", typeof(TEntity).Name, id, string.Join(",", includes));
var query = _db.Set<TEntity>().AsQueryable();
query = includes.Aggregate(query, (current, include) => current.Include(include));
return query.SingleOrDefaultAsync(c => c.Id.Equals(id));
}
// ...
}
}
- Create repositories CRUD unit tests
namespace StarWars.Tests.Unit.Data.EntityFramework.Repositories
{
public class HumanRepositoryShould
{
private readonly HumanRepository _humanRepository;
private DbContextOptions<StarWarsContext> _options;
private Mock<ILogger<StarWarsContext>> _dbLogger;
public HumanRepositoryShould()
{
// Given
_dbLogger = new Mock<ILogger<StarWarsContext>>();
_options = new DbContextOptionsBuilder<StarWarsContext>()
.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<ILogger<HumanRepository>>();
_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
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"]);
}
}
}