/StarWars

GraphQL 'Star Wars' example using GraphQL for .NET, ASP.NET Core, Entity Framework Core

Primary LanguageC#MIT LicenseMIT

GraphQL 'Star Wars' example using GraphQL for .NET, ASP.NET Core, Entity Framework Core

Build Status

Examples

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 empty-solution

  • Add 'ASP.NET Core Web Application (.NET Core)' project named 'StarWars.Api' aspnet-core-web-application

  • Select Web API template aspnet-core-web-api

  • Update all NuGet packages update-all-nuget-packages

  • Update project.json with correct runtime

"runtimes": {
   "win10-x64": { }
}
  • Install GraphQL NuGet package install-graphql-nuget-package

  • Create 'StarWars.Core' project 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 postman-test-query

  • 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 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 entity-framework-core-nuget entity-framework-sql-server-provider-nuget

  • 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 ssms-starwars-database-created

  • Final test using Postman postman-test-query

Entity Framework Migrations

  • Add 'Microsoft.EntityFrameworkCore.Design' NuGet package to 'StarWars.Data' project ef-design-nuget

  • Add 'Microsoft.EntityFrameworkCore.Tools.DotNet' NuGet package to 'StarWars.Data' project ef-tools-dotnet-nuget

  • 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

dotnet-ef-migrations

GrahpiQL

  • Add NPM configuration file 'package.json' to StarWars.Api project npm-configuration-file

  • 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 npm-task-runner

  • Configure 'After Build' step in 'Task Runner Explorer' after-build-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 static-files-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 bundles-created-by-webpack

  • Run project and enjoy GraphiQL graphiql

Unit Tests

  • Create 'Class Library (.NET Core)' type 'StarWars.Tests.Unit' project unit-tests-project

  • Install 'xunit' NuGet package in StarWars.Tests.Unit project xunit-nuget

  • Install 'dotnet-test-xunit' NuGet package in StarWars.Tests.Unit project dotnet-test-xunit-nuget

  • 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' test-explorer-first-unit-test

  • Run test - it should fail (we want to make sure that we are testing the right thing) first-unit-test-fail

  • 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 first-unit-test-pass

  • Install 'Moq' NuGet package moq-nuget

  • Install 'Microsoft.EntityFrameworkCore.InMemory' NuGet package ef-in-memory-nuget

  • 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 vs-2017-rc-upgrade

  • 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) xunit-runner-visualstudio-nuget

  • Install (xunit.runner.visualstudio) dependency (Microsoft.DotNet.InternalAbstractions) microsoft-dotnet-internal-abstractions-nuget

Integration Tests

  • Create 'xUnit Test Project (.NET Core)' type 'StarWars.Tests.Integration' project integration-tests-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 test-host-nuget

  • Use EF in memory database for 'Test' evironment

    • Install 'Microsoft.EntityFrameworkCore.InMemory' NuGet package ef-in-memory-nuget-api-project

    • 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 console-logs-1 console-logs-2

Code Coverage

  • Install OpenCover NuGet package open-cover-nuget

  • 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

open-cover-unit-tests-results

  • Install 'ReportGenerator' NuGet package report-generator-nuget

  • 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 open-cover-html-report-results

Continous Integration

  • Create new project in VSTS (Visual Studio Team Services) vsts-new-project

  • 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 vsts-new-build-definition vsts-new-build-definition-github-hosted-vs2017

  • Add new GitHub service connection vsts-new-github-service-connection

  • Setup repository vsts-setup-repository

  • Switch to "New Build Editor" vsts-new-build-editor

  • Setup build process (tasks, build steps) vsts-setup-build-steps

  • Setup projects in Test build step

**/Tests/StarWars.Tests.Unit/StarWars.Tests.Unit.csproj;**/Tests/StarWars.Tests.Integration/StarWars.Tests.Integration.csproj

vsts-setup-projects-test-build-setp

  • Queue build. Make sure it succeeded and executed unit and integration tests. vsts-queue-build vsts-build-succeeded

  • Enable build badge (after save you will see link to build status image). vsts-enable-badge

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 ef-tools-nuget

  • Set 'StarWars.Data' as a StartUp project

  • Add 'Full' migrations ef-full-migration

  • Update database ef-update-database-full ef-update-database-full-ssms

  • Set 'StarWars.Api' as a StartUp project

  • Run 'StarWars.Api' to seed database ef-seed-full-database seeded-full-database-smss

  • 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 all-tests-pass-full-database

  • 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 graphiql-full-database

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) vs-2017-rtm-nugets-update

  • 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. consolidate-netcore-app

Install-Package Microsoft.NETCore.App

package-manager-console-netcore-app-upgrade

  • 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 test-explorer-repositories-crud-tests code-coverage-repositories-crud-tests

GraphQL queries

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"]);
        }
    }
}

facebook-spec-queries-tdd-integration-tests-failed