diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d30ada --- /dev/null +++ b/.gitignore @@ -0,0 +1,331 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ +.DS_Store \ No newline at end of file diff --git a/dotnetsummitby_2019/GraphQL_Stitching_with_Hot_Chocolate.pdf b/bern_usergroup_2019/GraphQL_Stitching_with_Hot_Chocolate.pdf similarity index 100% rename from dotnetsummitby_2019/GraphQL_Stitching_with_Hot_Chocolate.pdf rename to bern_usergroup_2019/GraphQL_Stitching_with_Hot_Chocolate.pdf diff --git a/brighton_usergroup_2019/GraphQL_Stitching_with_Hot_Chocolate.pdf b/brighton_usergroup_2019/GraphQL_Stitching_with_Hot_Chocolate.pdf new file mode 100644 index 0000000..4c68b87 Binary files /dev/null and b/brighton_usergroup_2019/GraphQL_Stitching_with_Hot_Chocolate.pdf differ diff --git a/cluj_apexvox_2019/Introduction/1_graphql_vs_rest/index.html b/cluj_apexvox_2019/Introduction/1_graphql_vs_rest/index.html new file mode 100644 index 0000000..889c94b --- /dev/null +++ b/cluj_apexvox_2019/Introduction/1_graphql_vs_rest/index.html @@ -0,0 +1,268 @@ + + + + + GraphQL vs. REST + + + +
+ + vs. + +
+
+ Loading... +
+
+

+ An unsorted list of characters who played in the same films where Luke + Skywalker were present +

+

+ Used and returned + characters which took + ms. +

+ +
+ + + diff --git a/cluj_apexvox_2019/Introduction/2_hello_world/.vscode/launch.json b/cluj_apexvox_2019/Introduction/2_hello_world/.vscode/launch.json new file mode 100644 index 0000000..c509473 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/2_hello_world/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp3.0/2_hello_world.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/cluj_apexvox_2019/Introduction/2_hello_world/.vscode/tasks.json b/cluj_apexvox_2019/Introduction/2_hello_world/.vscode/tasks.json new file mode 100644 index 0000000..31c32bd --- /dev/null +++ b/cluj_apexvox_2019/Introduction/2_hello_world/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/cluj_apexvox_2019/Introduction/2_hello_world/2_hello_world.csproj b/cluj_apexvox_2019/Introduction/2_hello_world/2_hello_world.csproj new file mode 100644 index 0000000..b3d059f --- /dev/null +++ b/cluj_apexvox_2019/Introduction/2_hello_world/2_hello_world.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.0 + _2_hello_world + + + + + + + diff --git a/cluj_apexvox_2019/Introduction/2_hello_world/Program.cs b/cluj_apexvox_2019/Introduction/2_hello_world/Program.cs new file mode 100644 index 0000000..7a86352 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/2_hello_world/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace HelloWorld +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/cluj_apexvox_2019/Introduction/2_hello_world/Startup.cs b/cluj_apexvox_2019/Introduction/2_hello_world/Startup.cs new file mode 100644 index 0000000..15e44a2 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/2_hello_world/Startup.cs @@ -0,0 +1,22 @@ +using HotChocolate; +using HotChocolate.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace HelloWorld +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + } + } +} diff --git a/cluj_apexvox_2019/Introduction/2_hello_world/appsettings.Development.json b/cluj_apexvox_2019/Introduction/2_hello_world/appsettings.Development.json new file mode 100644 index 0000000..a2880cb --- /dev/null +++ b/cluj_apexvox_2019/Introduction/2_hello_world/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/cluj_apexvox_2019/Introduction/2_hello_world/appsettings.json b/cluj_apexvox_2019/Introduction/2_hello_world/appsettings.json new file mode 100644 index 0000000..81ff877 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/2_hello_world/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/cluj_apexvox_2019/Introduction/2_hello_world/copy.txt b/cluj_apexvox_2019/Introduction/2_hello_world/copy.txt new file mode 100644 index 0000000..e23c42a --- /dev/null +++ b/cluj_apexvox_2019/Introduction/2_hello_world/copy.txt @@ -0,0 +1,10 @@ +public class Query +{ + public string Hello() => "World"; +} + +services.AddGraphQL(sp => SchemaBuilder.New() + .AddQueryType() + .Create()); + +app.UseGraphQL(); \ No newline at end of file diff --git a/cluj_apexvox_2019/Introduction/2_hello_world/global.json b/cluj_apexvox_2019/Introduction/2_hello_world/global.json new file mode 100644 index 0000000..79422f0 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/2_hello_world/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "3.0.100" + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/.vscode/launch.json b/cluj_apexvox_2019/Introduction/3_operations/.vscode/launch.json new file mode 100644 index 0000000..d3f889e --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/StarWars/bin/Debug/netcoreapp3.0/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/cluj_apexvox_2019/Introduction/3_operations/.vscode/tasks.json b/cluj_apexvox_2019/Introduction/3_operations/.vscode/tasks.json new file mode 100644 index 0000000..383f2db --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/.vscode/tasks.json @@ -0,0 +1,25 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + "StarWars", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/.vscode/launch.json b/cluj_apexvox_2019/Introduction/3_operations/StarWars/.vscode/launch.json new file mode 100644 index 0000000..401f807 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.1/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/.vscode/tasks.json b/cluj_apexvox_2019/Introduction/3_operations/StarWars/.vscode/tasks.json new file mode 100644 index 0000000..68ae5ff --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet build", + "type": "shell", + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/AspNetCore.StarWars.csproj b/cluj_apexvox_2019/Introduction/3_operations/StarWars/AspNetCore.StarWars.csproj new file mode 100644 index 0000000..1b041de --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/AspNetCore.StarWars.csproj @@ -0,0 +1,34 @@ + + + + netcoreapp3.0 + false + 7.2 + true + $(NoWarn);1591 + + + + portable + true + + + + pdbonly + true + + + + + + + + + + + + + + + + diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Data/CharacterRepository.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Data/CharacterRepository.cs new file mode 100644 index 0000000..dd09134 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Data/CharacterRepository.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StarWars.Models; + +namespace StarWars.Data +{ + public class CharacterRepository + { + private Dictionary _characters; + private Dictionary _starships; + + public CharacterRepository() + { + _characters = CreateCharacters().ToDictionary(t => t.Id); + _starships = CreateStarships().ToDictionary(t => t.Id); + } + + public ICharacter GetHero(Episode episode) + { + if (episode == Episode.Empire) + { + return _characters["1000"]; + } + return _characters["2001"]; + } + + public ICharacter GetCharacter(string id) + { + if (_characters.TryGetValue(id, out ICharacter c)) + { + return c; + } + return null; + } + + public Human GetHuman(string id) + { + if (_characters.TryGetValue(id, out ICharacter c) + && c is Human h) + { + return h; + } + return null; + } + + public Droid GetDroid(string id) + { + if (_characters.TryGetValue(id, out ICharacter c) + && c is Droid d) + { + return d; + } + return null; + } + + public IEnumerable Search(string text) + { +#if ASPNETCLASSIC + IEnumerable filteredCharacters = _characters.Values + .Where(t => t.Name.Contains(text)); +#else + IEnumerable filteredCharacters = _characters.Values + .Where(t => t.Name.Contains(text, + StringComparison.OrdinalIgnoreCase)); +#endif + + foreach (ICharacter character in filteredCharacters) + { + yield return character; + } + +#if ASPNETCLASSIC + IEnumerable filteredStarships = _starships.Values + .Where(t => t.Name.Contains(text)); +#else + IEnumerable filteredStarships = _starships.Values + .Where(t => t.Name.Contains(text, + StringComparison.OrdinalIgnoreCase)); +#endif + + foreach (Starship starship in filteredStarships) + { + yield return starship; + } + } + + private static IEnumerable CreateCharacters() + { + yield return new Human + { + Id = "1000", + Name = "Luke Skywalker", + Friends = new[] { "1002", "1003", "2000", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Tatooine" + }; + + yield return new Human + { + Id = "1001", + Name = "Darth Vader", + Friends = new[] { "1004" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Tatooine" + }; + + yield return new Human + { + Id = "1002", + Name = "Han Solo", + Friends = new[] { "1000", "1003", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi } + }; + + yield return new Human + { + Id = "1003", + Name = "Leia Organa", + Friends = new[] { "1000", "1002", "2000", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Alderaan" + }; + + yield return new Human + { + Id = "1004", + Name = "Wilhuff Tarkin", + Friends = new[] { "1001" }, + AppearsIn = new[] { Episode.NewHope } + }; + + yield return new Droid + { + Id = "2000", + Name = "C-3PO", + Friends = new[] { "1000", "1002", "1003", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + PrimaryFunction = "Protocol" + }; + + yield return new Droid + { + Id = "2001", + Name = "R2-D2", + Friends = new[] { "1000", "1002", "1003" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + PrimaryFunction = "Astromech" + }; + } + + private static IEnumerable CreateStarships() + { + yield return new Starship + { + Id = "3000", + Name = "TIE Advanced x1", + Length = 9.2 + }; + } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Data/ReviewRepository.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Data/ReviewRepository.cs new file mode 100644 index 0000000..a586306 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Data/ReviewRepository.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using StarWars.Models; + +namespace StarWars.Data +{ + public class ReviewRepository + { + private readonly Dictionary> _data = + new Dictionary>(); + + public void AddReview(Episode episode, Review review) + { + if (!_data.TryGetValue(episode, out List reviews)) + { + reviews = new List(); + _data[episode] = reviews; + } + + reviews.Add(review); + } + + public IEnumerable GetReviews(Episode episode) + { + if (_data.TryGetValue(episode, out List reviews)) + { + return reviews; + } + return Array.Empty(); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Droid.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Droid.cs new file mode 100644 index 0000000..0ee4213 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Droid.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A droid in the Star Wars universe. + /// + public class Droid + : ICharacter + { + /// + public string Id { get; set; } + + /// + public string Name { get; set; } + + /// + public IReadOnlyList Friends { get; set; } + + /// + public IReadOnlyList AppearsIn { get; set; } + + /// + /// The droid's primary function. + /// + public string PrimaryFunction { get; set; } + + /// + public double Height { get; } = 1.72d; + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Episode.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Episode.cs new file mode 100644 index 0000000..6900cf6 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Episode.cs @@ -0,0 +1,21 @@ +namespace StarWars.Models +{ + /// + /// The Star Wars episodes. + /// + public enum Episode + { + /// + /// Star Wars Episode IV: A New Hope + /// + NewHope, + /// + /// Star Wars Episode V: Empire Strikes Back + /// + Empire, + /// + /// Star Wars Episode VI: Return of the Jedi + /// + Jedi + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Human.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Human.cs new file mode 100644 index 0000000..caf6021 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Human.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A human character in the Star Wars universe. + /// + public class Human + : ICharacter + { + /// + public string Id { get; set; } + + /// + public string Name { get; set; } + + /// + public IReadOnlyList Friends { get; set; } + + /// + public IReadOnlyList AppearsIn { get; set; } + + /// + /// The planet the character is originally from. + /// + public string HomePlanet { get; set; } + + /// + public double Height { get; } = 1.72d; + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/ICharacter.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/ICharacter.cs new file mode 100644 index 0000000..f9186c1 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/ICharacter.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A character in the Star Wars universe. + /// + public interface ICharacter + { + /// + /// The unique identifier for the character. + /// + string Id { get; } + + /// + /// The name of the character. + /// + string Name { get; } + + /// + /// The names of the character's friends. + /// + IReadOnlyList Friends { get; } + + /// + /// The episodes the character appears in. + /// + IReadOnlyList AppearsIn { get; } + + /// + /// The height of the character. + /// + double Height { get; } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Review.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Review.cs new file mode 100644 index 0000000..3f18f16 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Review.cs @@ -0,0 +1,18 @@ +namespace StarWars.Models +{ + /// + /// A review of a particular movie. + /// + public class Review + { + /// + /// The number of stars given for this review. + /// + public int Stars { get; set; } + + /// + /// An explanation for the rating. + /// + public string Commentary { get; set; } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Starship.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Starship.cs new file mode 100644 index 0000000..5d7c241 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Starship.cs @@ -0,0 +1,23 @@ +namespace StarWars.Models +{ + /// + /// A starship in the Star Wars universe. + /// + public class Starship + { + /// + /// The Id of the starship. + /// + public string Id { get; set; } + + /// + /// The name of the starship. + /// + public string Name { get; set; } + + /// + /// The length of the starship. + /// + public double Length { get; set; } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Unit.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Unit.cs new file mode 100644 index 0000000..15316aa --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Models/Unit.cs @@ -0,0 +1,11 @@ +namespace StarWars.Models +{ + /// + /// Different units of measurement. + /// + public enum Unit + { + Foot, + Meters + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Mutation.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Mutation.cs new file mode 100644 index 0000000..45d4adc --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Mutation.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Subscriptions; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Mutation + { + private readonly ReviewRepository _repository; + + public Mutation(ReviewRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Creates a review for a given Star Wars episode. + /// + /// The episode to review. + /// The review. + /// The event sending service. + /// The created review. + public async Task CreateReview( + Episode episode, Review review, + [Service]IEventSender eventSender) + { + _repository.AddReview(episode, review); + await eventSender.SendAsync(new OnReviewMessage(episode, review)); + return review; + } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/OnReviewMessage.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/OnReviewMessage.cs new file mode 100644 index 0000000..95f9cff --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/OnReviewMessage.cs @@ -0,0 +1,23 @@ +using HotChocolate.Language; +using HotChocolate.Subscriptions; +using StarWars.Models; + +namespace StarWars +{ + public class OnReviewMessage + : EventMessage + { + public OnReviewMessage(Episode episode, Review review) + : base(CreateEventDescription(episode), review) + { + } + + private static EventDescription CreateEventDescription(Episode episode) + { + return new EventDescription("onReview", + new ArgumentNode("episode", + new EnumValueNode( + episode.ToString().ToUpperInvariant()))); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Program.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Program.cs new file mode 100644 index 0000000..0358799 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace StarWars +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Query.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Query.cs new file mode 100644 index 0000000..887534e --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Query.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using HotChocolate.Resolvers; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Query + { + private readonly CharacterRepository _repository; + + public Query(CharacterRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Retrieve a hero by a particular Star Wars episode. + /// + /// The episode to look up by. + /// The character. + public ICharacter GetHero(Episode episode) + { + return _repository.GetHero(episode); + } + + /// + /// Gets a human by Id. + /// + /// The Id of the human to retrieve. + /// The human. + public Human GetHuman(string id) + { + return _repository.GetHuman(id); + } + + /// + /// Get a particular droid by Id. + /// + /// The Id of the droid. + /// The droid. + public Droid GetDroid(string id) + { + return _repository.GetDroid(id); + } + + public IEnumerable GetCharacter(string[] characterIds, IResolverContext context) + { + foreach (string characterId in characterIds) + { + ICharacter character = _repository.GetCharacter(characterId); + if (character == null) + { + context.ReportError( + "Could not resolve a charachter for the " + + $"character-id {characterId}."); + } + else + { + yield return character; + } + } + } + + public IEnumerable Search(string text) + { + return _repository.Search(text); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Resolvers/SharedResolvers.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Resolvers/SharedResolvers.cs new file mode 100644 index 0000000..3911604 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Resolvers/SharedResolvers.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using HotChocolate; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars.Resolvers +{ + public class SharedResolvers + { + public IEnumerable GetCharacter( + [Parent]ICharacter character, + [Service]CharacterRepository repository) + { + foreach (string friendId in character.Friends) + { + ICharacter friend = repository.GetCharacter(friendId); + if (friend != null) + { + yield return friend; + } + } + } + + public double GetHeight(Unit? unit, [Parent]ICharacter character) + => ConvertToUnit(character.Height, unit); + + public double GetLength(Unit? unit, [Parent]Starship starship) + => ConvertToUnit(starship.Length, unit); + + private double ConvertToUnit(double length, Unit? unit) + { + if (unit == Unit.Foot) + { + return length * 3.28084d; + } + return length; + } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Startup.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Startup.cs new file mode 100644 index 0000000..b55e4d8 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Startup.cs @@ -0,0 +1,66 @@ +using System.Security.Claims; +using HotChocolate; +using HotChocolate.AspNetCore; +using HotChocolate.AspNetCore.Voyager; +using HotChocolate.Execution.Configuration; +using HotChocolate.Subscriptions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using StarWars.Data; +using StarWars.Types; + +namespace StarWars +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + // Add the custom services like repositories etc ... + services.AddSingleton(); + services.AddSingleton(); + + // Add in-memory event provider + services.AddInMemorySubscriptionProvider(); + + // Add GraphQL Services + services.AddGraphQL(sp => SchemaBuilder.New() + .AddServices(sp) + + // Adds the authorize directive and + // enable the authorization middleware. + .AddAuthorizeDirectiveType() + + .AddQueryType() + .AddMutationType() + .AddSubscriptionType() + .AddType() + .AddType() + .AddType() + .Create()); + + + // Add Authorization Policy + services.AddAuthorization(options => + { + options.AddPolicy("HasCountry", policy => + policy.RequireAssertion(context => + context.User.HasClaim(c => + (c.Type == ClaimTypes.Country)))); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app + .UseWebSockets() + .UseGraphQL("/graphql") + .UseGraphiQL("/graphql") + .UsePlayground("/graphql") + .UseVoyager("/graphql"); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Subscription.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Subscription.cs new file mode 100644 index 0000000..06a88b2 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Subscription.cs @@ -0,0 +1,23 @@ +using System; +using HotChocolate.Subscriptions; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Subscription + { + private readonly ReviewRepository _repository; + + public Subscription(ReviewRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + public Review OnReview(Episode episode, IEventMessage message) + { + return (Review)message.Payload; + } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/CharacterType.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/CharacterType.cs new file mode 100644 index 0000000..b9c5c98 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/CharacterType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; + +namespace StarWars.Types +{ + public class CharacterType + : InterfaceType + { + protected override void Configure(IInterfaceTypeDescriptor descriptor) + { + descriptor.Name("Character"); + + descriptor.Field(f => f.Id) + .Type>(); + + descriptor.Field(f => f.Name) + .Type(); + + descriptor.Field(f => f.Friends) + .UsePaging(); + + descriptor.Field(f => f.AppearsIn) + .Type>(); + + descriptor.Field(f => f.Height) + .Type() + .Argument("unit", a => a.Type>()); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/DroidType.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/DroidType.cs new file mode 100644 index 0000000..c34fe9a --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/DroidType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class DroidType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.AppearsIn) + .Type>(); + + descriptor.Field(r => r.GetCharacter(default, default)) + .UsePaging() + .Name("friends"); + + descriptor.Field(t => t.GetHeight(default, default)) + .Type() + .Argument("unit", a => a.Type>()) + .Name("height"); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/EpisodeType.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/EpisodeType.cs new file mode 100644 index 0000000..bfb484d --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/EpisodeType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class EpisodeType + : EnumType + { + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/HumanType.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/HumanType.cs new file mode 100644 index 0000000..3ab1c6a --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/HumanType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class HumanType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.AppearsIn) + .Type>(); + + descriptor.Field(r => r.GetCharacter(default, default)) + .UsePaging() + .Name("friends"); + + descriptor.Field(t => t.GetHeight(default, default)) + .Type() + .Argument("unit", a => a.Type>()) + .Name("height"); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/MutationType.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/MutationType.cs new file mode 100644 index 0000000..71963b7 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/MutationType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Types; + +namespace StarWars.Types +{ + public class MutationType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.CreateReview(default, default, default)) + .Type>() + .Argument("episode", a => a.Type>()) + .Argument("review", a => a.Type>()); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/QueryType.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/QueryType.cs new file mode 100644 index 0000000..193cf89 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/QueryType.cs @@ -0,0 +1,22 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class QueryType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetHero(default)) + .Type() + .Argument("episode", a => a.DefaultValue(Episode.NewHope)); + + descriptor.Field(t => t.GetCharacter(default, default)) + .Type>>>(); + + descriptor.Field(t => t.Search(default)) + .Type>(); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/ReviewInputType.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/ReviewInputType.cs new file mode 100644 index 0000000..300fbe6 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/ReviewInputType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class ReviewInputType + : InputObjectType + { + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/ReviewType.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/ReviewType.cs new file mode 100644 index 0000000..be94b38 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/ReviewType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class ReviewType + : ObjectType + { + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/SearchResultType.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/SearchResultType.cs new file mode 100644 index 0000000..7d1bb88 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/SearchResultType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class SearchResultType + : UnionType + { + protected override void Configure(IUnionTypeDescriptor descriptor) + { + descriptor.Name("SearchResult"); + descriptor.Type>(); + descriptor.Type(); + descriptor.Type(); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/StarshipType.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/StarshipType.cs new file mode 100644 index 0000000..7ad6b10 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/StarshipType.cs @@ -0,0 +1,18 @@ +using HotChocolate.Types; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class StarshipType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.GetLength(default, default)); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/SubscriptionType.cs b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/SubscriptionType.cs new file mode 100644 index 0000000..ed872e5 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/StarWars/Types/SubscriptionType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace StarWars.Types +{ + public class SubscriptionType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.OnReview(default, default)) + .Type>() + .Argument("episode", arg => arg.Type>()); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/3_operations/global.json b/cluj_apexvox_2019/Introduction/3_operations/global.json new file mode 100644 index 0000000..79422f0 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/3_operations/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "3.0.100" + } +} diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/DataLoader.csproj b/cluj_apexvox_2019/Introduction/4_dataloader/DataLoader.csproj new file mode 100644 index 0000000..2a8ff13 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/DataLoader.csproj @@ -0,0 +1,25 @@ + + + + netcoreapp2.2 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + \ No newline at end of file diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/DataLoader.sln b/cluj_apexvox_2019/Introduction/4_dataloader/DataLoader.sln new file mode 100644 index 0000000..f90e842 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/DataLoader.sln @@ -0,0 +1,17 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataLoader", "DataLoader.csproj", "{8B0F2085-502A-495C-BD9D-9827B8F3F0EE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8B0F2085-502A-495C-BD9D-9827B8F3F0EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B0F2085-502A-495C-BD9D-9827B8F3F0EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B0F2085-502A-495C-BD9D-9827B8F3F0EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B0F2085-502A-495C-BD9D-9827B8F3F0EE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/Message.cs b/cluj_apexvox_2019/Introduction/4_dataloader/Message.cs new file mode 100644 index 0000000..5a16590 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/Message.cs @@ -0,0 +1,16 @@ +using System; +using MongoDB.Bson; +using HotChocolate.Language; + +namespace HotChocolate.Examples.Paging +{ + public class Message + { + public ObjectId Id { get; set; } + public string Text { get; set; } + public DateTimeOffset Created { get; set; } + public int Favorites { get; set; } + public ObjectId UserId { get; set; } + public ObjectId? ReplyToId { get; set; } + } +} diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/MessageInput.cs b/cluj_apexvox_2019/Introduction/4_dataloader/MessageInput.cs new file mode 100644 index 0000000..22b7823 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/MessageInput.cs @@ -0,0 +1,11 @@ +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class MessageInput + { + public string Text { get; set; } + public ObjectId UserId { get; set; } + public ObjectId? ReplyToId { get; set; } + } +} diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/MessageInputType.cs b/cluj_apexvox_2019/Introduction/4_dataloader/MessageInputType.cs new file mode 100644 index 0000000..249be7d --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/MessageInputType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class MessageInputType + : InputObjectType + { + protected override void Configure(IInputObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Text).Type>(); + descriptor.Field(t => t.UserId).Type>(); + descriptor.Field(t => t.ReplyToId).Type(); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/MessageRepository.cs b/cluj_apexvox_2019/Introduction/4_dataloader/MessageRepository.cs new file mode 100644 index 0000000..ad224ae --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/MessageRepository.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace HotChocolate.Examples.Paging +{ + public class MessageRepository + { + private readonly IMongoCollection _messageCollection; + + public MessageRepository(IMongoCollection messageCollection) + { + _messageCollection = messageCollection + ?? throw new ArgumentNullException(nameof(messageCollection)); + } + + public IQueryable GetAllMessages() + { + return _messageCollection.AsQueryable(); + } + + public Task GetMessageById(ObjectId messageId) + { + return _messageCollection.AsQueryable().FirstOrDefaultAsync(t => t.Id == messageId); + } + + public Task CreateMessageAsync(Message message, CancellationToken cancellationToken) + { + return _messageCollection.InsertOneAsync(message, new InsertOneOptions(), cancellationToken); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/MessageType.cs b/cluj_apexvox_2019/Introduction/4_dataloader/MessageType.cs new file mode 100644 index 0000000..6eb2dca --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/MessageType.cs @@ -0,0 +1,34 @@ +using HotChocolate.Resolvers; +using HotChocolate.Types; +using GreenDonut; +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class MessageType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Text).Type>(); + descriptor.Field("createdBy").Type>().Resolver(ctx => + { + UserRepository repository = ctx.Service(); + return repository.GetUserAsync(ctx.Parent().UserId, ctx.RequestAborted); + }); + descriptor.Field("replyTo").Type().Resolver(async ctx => + { + ObjectId? replyToId = ctx.Parent().ReplyToId; + if (replyToId.HasValue) + { + MessageRepository repository = ctx.Service(); + return await repository.GetMessageById(replyToId.Value); + } + return null; + }); + descriptor.Ignore(t => t.UserId); + descriptor.Ignore(t => t.ReplyToId); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/Mutation.cs b/cluj_apexvox_2019/Introduction/4_dataloader/Mutation.cs new file mode 100644 index 0000000..2aae183 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/Mutation.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace HotChocolate.Examples.Paging +{ + public class Mutation + { + public async Task CreateMessageAsync( + MessageInput messageInput, + [Service]MessageRepository repository, + CancellationToken cancellationToken) + { + var message = new Message + { + Text = messageInput.Text, + UserId = messageInput.UserId, + ReplyToId = messageInput.ReplyToId, + Created = DateTimeOffset.UtcNow, + }; + + await repository.CreateMessageAsync(message, cancellationToken); + + return message; + } + + public async Task CreateUserAsync( + UserInput userInput, + [Service]UserRepository repository, + CancellationToken cancellationToken) + { + var user = new User + { + Name = userInput.Name, + Country = userInput.Country + }; + + await repository.CreateUserAsync(user, cancellationToken); + + return user; + } + } +} diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/MutationType.cs b/cluj_apexvox_2019/Introduction/4_dataloader/MutationType.cs new file mode 100644 index 0000000..18bfcef --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/MutationType.cs @@ -0,0 +1,19 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class MutationType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.CreateMessageAsync(default, default, default)) + .Argument("messageInput", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.CreateUserAsync(default, default, default)) + .Argument("userInput", a => a.Type>()) + .Type(); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/Program.cs b/cluj_apexvox_2019/Introduction/4_dataloader/Program.cs new file mode 100644 index 0000000..3c1cee5 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace HotChocolate.Examples.Paging +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/QueryType.cs b/cluj_apexvox_2019/Introduction/4_dataloader/QueryType.cs new file mode 100644 index 0000000..164a960 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/QueryType.cs @@ -0,0 +1,33 @@ +using GreenDonut; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using HotChocolate.Types.Relay; + +namespace HotChocolate.Examples.Paging +{ + public class QueryType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field("messages") + .Type>() + .Resolver(ctx => ctx.Service().GetAllMessages()); + + descriptor.Field("usersByCountry") + .Argument("country", a => a.Type>()) + .Type>>>() + .Resolver(ctx => + { + var userRepository = ctx.Service(); + + IDataLoader userDataLoader = + ctx.GroupDataLoader( + "usersByCountry", + userRepository.GetUsersByCountry); + + return userDataLoader.LoadAsync(ctx.Argument("country")); + }); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/README.md b/cluj_apexvox_2019/Introduction/4_dataloader/README.md new file mode 100644 index 0000000..760b464 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/README.md @@ -0,0 +1,21 @@ +# DataLoader Example + +This example shows how DataLoader can be used with Hot Chocolate and uses _Mongo_ as database. + +The **Cache DataLoader** and the **Batch DataLoader** are used in `MessageType.cs`. + +The **GroupDataLoader** is used in `QueryType.cs`. + +We support more DataLoader scenarious with Hot Chocolate than are showcased with this example. The example is aimed to show the most common use-cases. + +## Setup Mongo + +Personally I used docker to host my mongo db for the example. If you have setup docker that just add the following line in your terminal emulator of choice: + +```bash +docker run --name mongo -p 27017:27017 -d mongo mongod +``` + +If you don't have docker or do not want to use it you can install mongo from here: [https://www.mongodb.com/download-center/community](https://www.mongodb.com/download-center/community). + +[Hot Chocolate Documentation](https://hotchocolate.io) diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/Startup.cs b/cluj_apexvox_2019/Introduction/4_dataloader/Startup.cs new file mode 100644 index 0000000..d445da4 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/Startup.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.AspNetCore; +using HotChocolate; +using HotChocolate.Execution.Configuration; +using MongoDB.Driver; +using HotChocolate.Utilities; +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + // setup type conversion for object id + TypeConversion.Default.Register(from => ObjectId.Parse(from)); + TypeConversion.Default.Register(from => from.ToString()); + + // setup the repositories + services.AddSingleton(new MongoClient("mongodb://127.0.0.1:27017")); + services.AddSingleton(s => s.GetRequiredService().GetDatabase("PagingDemo")); + services.AddSingleton>(s => s.GetRequiredService().GetCollection("messages")); + services.AddSingleton>(s => s.GetRequiredService().GetCollection("users")); + services.AddSingleton(); + services.AddSingleton(); + + // this enables you to use DataLoader in your resolvers. + services.AddDataLoaderRegistry(); + + // Add GraphQL Services + services.AddGraphQL(SchemaBuilder.New() + .AddQueryType() + .AddMutationType()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(); + app.UsePlayground(); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/User.cs b/cluj_apexvox_2019/Introduction/4_dataloader/User.cs new file mode 100644 index 0000000..155c78b --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/User.cs @@ -0,0 +1,11 @@ +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class User + { + public ObjectId Id { get; set; } + public string Name { get; set; } + public string Country { get; set; } + } +} diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/UserInput.cs b/cluj_apexvox_2019/Introduction/4_dataloader/UserInput.cs new file mode 100644 index 0000000..2511647 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/UserInput.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.Examples.Paging +{ + public class UserInput + { + public string Name { get; set; } + public string Country { get; set; } + } +} diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/UserInputType.cs b/cluj_apexvox_2019/Introduction/4_dataloader/UserInputType.cs new file mode 100644 index 0000000..1e38cc0 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/UserInputType.cs @@ -0,0 +1,14 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class UserInputType + : InputObjectType + { + protected override void Configure(IInputObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Name).Type>(); + descriptor.Field(t => t.Country).Type>(); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/UserRepository.cs b/cluj_apexvox_2019/Introduction/4_dataloader/UserRepository.cs new file mode 100644 index 0000000..90399ed --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/UserRepository.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace HotChocolate.Examples.Paging +{ + public class UserRepository + { + private readonly IMongoCollection _userCollection; + + public UserRepository(IMongoCollection userCollection) + { + _userCollection = userCollection + ?? throw new ArgumentNullException(nameof(userCollection)); + } + + public IQueryable GetAllUsers() + { + return _userCollection.AsQueryable(); + } + + public async Task> GetUsersByCountry( + IReadOnlyList countries, + CancellationToken cancellationToken) + { + var filters = new List>(); + + foreach (string country in countries) + { + filters.Add(Builders.Filter.Eq(u => u.Country, country)); + } + + List users = await _userCollection + .Find(Builders.Filter.Or(filters)) + .ToListAsync(cancellationToken); + + return users.ToLookup(t => t.Country); + } + + public Task GetUserAsync(ObjectId userId, CancellationToken cancellationToken) + { + return _userCollection.Find(c => c.Id == userId) + .FirstOrDefaultAsync(cancellationToken); + } + + public Task CreateUserAsync(User user, CancellationToken cancellationToken) + { + return _userCollection.InsertOneAsync(user, new InsertOneOptions(), cancellationToken); + } + + public async Task> GetUsersAsync( + IReadOnlyCollection userIds, + CancellationToken cancellationToken) + { + var filters = new List>(); + foreach (ObjectId userId in userIds) + { + filters.Add(Builders.Filter.Eq(u => u.Id, userId)); + } + + List users = await _userCollection + .Find(Builders.Filter.Or(filters)) + .ToListAsync(cancellationToken); + + return users.ToDictionary(t => t.Id); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/UserType.cs b/cluj_apexvox_2019/Introduction/4_dataloader/UserType.cs new file mode 100644 index 0000000..7af7a05 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/UserType.cs @@ -0,0 +1,14 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class UserType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Name).Type>(); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/copy.txt b/cluj_apexvox_2019/Introduction/4_dataloader/copy.txt new file mode 100644 index 0000000..4248412 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/copy.txt @@ -0,0 +1,23 @@ +MessageType + +descriptor.Field("createdBy").Type>().Resolver(ctx => +{ + UserRepository repository = ctx.Service(); + + IDataLoader dataLoader = ctx.BatchDataLoader( + "UserById", + repository.GetUsersAsync); + + return dataLoader.LoadAsync(ctx.Parent().UserId); +}); + + +IDataLoader dataLoader = ctx.CacheDataLoader( + "MessageById", + repository.GetMessageById); + +return await dataLoader.LoadAsync(ctx.Parent().ReplyToId.Value); + +.UsePaging() + +docker run --name mongo -p 27017:27017 -d mongo mongod \ No newline at end of file diff --git a/cluj_apexvox_2019/Introduction/4_dataloader/global.json b/cluj_apexvox_2019/Introduction/4_dataloader/global.json new file mode 100644 index 0000000..89b3b0f --- /dev/null +++ b/cluj_apexvox_2019/Introduction/4_dataloader/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "2.2.402" + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/.vscode/launch.json b/cluj_apexvox_2019/Introduction/5_persisted_queries/.vscode/launch.json new file mode 100644 index 0000000..d3f889e --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/StarWars/bin/Debug/netcoreapp3.0/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/.vscode/tasks.json b/cluj_apexvox_2019/Introduction/5_persisted_queries/.vscode/tasks.json new file mode 100644 index 0000000..383f2db --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/.vscode/tasks.json @@ -0,0 +1,25 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + "StarWars", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/.vscode/launch.json b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/.vscode/launch.json new file mode 100644 index 0000000..401f807 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.1/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/.vscode/tasks.json b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/.vscode/tasks.json new file mode 100644 index 0000000..68ae5ff --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet build", + "type": "shell", + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/AspNetCore.StarWars.csproj b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/AspNetCore.StarWars.csproj new file mode 100644 index 0000000..841febc --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/AspNetCore.StarWars.csproj @@ -0,0 +1,32 @@ + + + + netcoreapp3.0 + false + 7.2 + true + $(NoWarn);1591 + + + + portable + true + + + + pdbonly + true + + + + + + + + + + + + + + diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Data/CharacterRepository.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Data/CharacterRepository.cs new file mode 100644 index 0000000..dd09134 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Data/CharacterRepository.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StarWars.Models; + +namespace StarWars.Data +{ + public class CharacterRepository + { + private Dictionary _characters; + private Dictionary _starships; + + public CharacterRepository() + { + _characters = CreateCharacters().ToDictionary(t => t.Id); + _starships = CreateStarships().ToDictionary(t => t.Id); + } + + public ICharacter GetHero(Episode episode) + { + if (episode == Episode.Empire) + { + return _characters["1000"]; + } + return _characters["2001"]; + } + + public ICharacter GetCharacter(string id) + { + if (_characters.TryGetValue(id, out ICharacter c)) + { + return c; + } + return null; + } + + public Human GetHuman(string id) + { + if (_characters.TryGetValue(id, out ICharacter c) + && c is Human h) + { + return h; + } + return null; + } + + public Droid GetDroid(string id) + { + if (_characters.TryGetValue(id, out ICharacter c) + && c is Droid d) + { + return d; + } + return null; + } + + public IEnumerable Search(string text) + { +#if ASPNETCLASSIC + IEnumerable filteredCharacters = _characters.Values + .Where(t => t.Name.Contains(text)); +#else + IEnumerable filteredCharacters = _characters.Values + .Where(t => t.Name.Contains(text, + StringComparison.OrdinalIgnoreCase)); +#endif + + foreach (ICharacter character in filteredCharacters) + { + yield return character; + } + +#if ASPNETCLASSIC + IEnumerable filteredStarships = _starships.Values + .Where(t => t.Name.Contains(text)); +#else + IEnumerable filteredStarships = _starships.Values + .Where(t => t.Name.Contains(text, + StringComparison.OrdinalIgnoreCase)); +#endif + + foreach (Starship starship in filteredStarships) + { + yield return starship; + } + } + + private static IEnumerable CreateCharacters() + { + yield return new Human + { + Id = "1000", + Name = "Luke Skywalker", + Friends = new[] { "1002", "1003", "2000", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Tatooine" + }; + + yield return new Human + { + Id = "1001", + Name = "Darth Vader", + Friends = new[] { "1004" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Tatooine" + }; + + yield return new Human + { + Id = "1002", + Name = "Han Solo", + Friends = new[] { "1000", "1003", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi } + }; + + yield return new Human + { + Id = "1003", + Name = "Leia Organa", + Friends = new[] { "1000", "1002", "2000", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Alderaan" + }; + + yield return new Human + { + Id = "1004", + Name = "Wilhuff Tarkin", + Friends = new[] { "1001" }, + AppearsIn = new[] { Episode.NewHope } + }; + + yield return new Droid + { + Id = "2000", + Name = "C-3PO", + Friends = new[] { "1000", "1002", "1003", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + PrimaryFunction = "Protocol" + }; + + yield return new Droid + { + Id = "2001", + Name = "R2-D2", + Friends = new[] { "1000", "1002", "1003" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + PrimaryFunction = "Astromech" + }; + } + + private static IEnumerable CreateStarships() + { + yield return new Starship + { + Id = "3000", + Name = "TIE Advanced x1", + Length = 9.2 + }; + } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Data/ReviewRepository.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Data/ReviewRepository.cs new file mode 100644 index 0000000..a586306 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Data/ReviewRepository.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using StarWars.Models; + +namespace StarWars.Data +{ + public class ReviewRepository + { + private readonly Dictionary> _data = + new Dictionary>(); + + public void AddReview(Episode episode, Review review) + { + if (!_data.TryGetValue(episode, out List reviews)) + { + reviews = new List(); + _data[episode] = reviews; + } + + reviews.Add(review); + } + + public IEnumerable GetReviews(Episode episode) + { + if (_data.TryGetValue(episode, out List reviews)) + { + return reviews; + } + return Array.Empty(); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Droid.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Droid.cs new file mode 100644 index 0000000..0ee4213 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Droid.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A droid in the Star Wars universe. + /// + public class Droid + : ICharacter + { + /// + public string Id { get; set; } + + /// + public string Name { get; set; } + + /// + public IReadOnlyList Friends { get; set; } + + /// + public IReadOnlyList AppearsIn { get; set; } + + /// + /// The droid's primary function. + /// + public string PrimaryFunction { get; set; } + + /// + public double Height { get; } = 1.72d; + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Episode.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Episode.cs new file mode 100644 index 0000000..6900cf6 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Episode.cs @@ -0,0 +1,21 @@ +namespace StarWars.Models +{ + /// + /// The Star Wars episodes. + /// + public enum Episode + { + /// + /// Star Wars Episode IV: A New Hope + /// + NewHope, + /// + /// Star Wars Episode V: Empire Strikes Back + /// + Empire, + /// + /// Star Wars Episode VI: Return of the Jedi + /// + Jedi + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Human.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Human.cs new file mode 100644 index 0000000..caf6021 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Human.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A human character in the Star Wars universe. + /// + public class Human + : ICharacter + { + /// + public string Id { get; set; } + + /// + public string Name { get; set; } + + /// + public IReadOnlyList Friends { get; set; } + + /// + public IReadOnlyList AppearsIn { get; set; } + + /// + /// The planet the character is originally from. + /// + public string HomePlanet { get; set; } + + /// + public double Height { get; } = 1.72d; + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/ICharacter.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/ICharacter.cs new file mode 100644 index 0000000..f9186c1 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/ICharacter.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A character in the Star Wars universe. + /// + public interface ICharacter + { + /// + /// The unique identifier for the character. + /// + string Id { get; } + + /// + /// The name of the character. + /// + string Name { get; } + + /// + /// The names of the character's friends. + /// + IReadOnlyList Friends { get; } + + /// + /// The episodes the character appears in. + /// + IReadOnlyList AppearsIn { get; } + + /// + /// The height of the character. + /// + double Height { get; } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Review.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Review.cs new file mode 100644 index 0000000..3f18f16 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Review.cs @@ -0,0 +1,18 @@ +namespace StarWars.Models +{ + /// + /// A review of a particular movie. + /// + public class Review + { + /// + /// The number of stars given for this review. + /// + public int Stars { get; set; } + + /// + /// An explanation for the rating. + /// + public string Commentary { get; set; } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Starship.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Starship.cs new file mode 100644 index 0000000..5d7c241 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Starship.cs @@ -0,0 +1,23 @@ +namespace StarWars.Models +{ + /// + /// A starship in the Star Wars universe. + /// + public class Starship + { + /// + /// The Id of the starship. + /// + public string Id { get; set; } + + /// + /// The name of the starship. + /// + public string Name { get; set; } + + /// + /// The length of the starship. + /// + public double Length { get; set; } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Unit.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Unit.cs new file mode 100644 index 0000000..15316aa --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Models/Unit.cs @@ -0,0 +1,11 @@ +namespace StarWars.Models +{ + /// + /// Different units of measurement. + /// + public enum Unit + { + Foot, + Meters + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Mutation.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Mutation.cs new file mode 100644 index 0000000..45d4adc --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Mutation.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Subscriptions; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Mutation + { + private readonly ReviewRepository _repository; + + public Mutation(ReviewRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Creates a review for a given Star Wars episode. + /// + /// The episode to review. + /// The review. + /// The event sending service. + /// The created review. + public async Task CreateReview( + Episode episode, Review review, + [Service]IEventSender eventSender) + { + _repository.AddReview(episode, review); + await eventSender.SendAsync(new OnReviewMessage(episode, review)); + return review; + } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/OnReviewMessage.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/OnReviewMessage.cs new file mode 100644 index 0000000..95f9cff --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/OnReviewMessage.cs @@ -0,0 +1,23 @@ +using HotChocolate.Language; +using HotChocolate.Subscriptions; +using StarWars.Models; + +namespace StarWars +{ + public class OnReviewMessage + : EventMessage + { + public OnReviewMessage(Episode episode, Review review) + : base(CreateEventDescription(episode), review) + { + } + + private static EventDescription CreateEventDescription(Episode episode) + { + return new EventDescription("onReview", + new ArgumentNode("episode", + new EnumValueNode( + episode.ToString().ToUpperInvariant()))); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Program.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Program.cs new file mode 100644 index 0000000..0358799 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace StarWars +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Query.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Query.cs new file mode 100644 index 0000000..887534e --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Query.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using HotChocolate.Resolvers; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Query + { + private readonly CharacterRepository _repository; + + public Query(CharacterRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Retrieve a hero by a particular Star Wars episode. + /// + /// The episode to look up by. + /// The character. + public ICharacter GetHero(Episode episode) + { + return _repository.GetHero(episode); + } + + /// + /// Gets a human by Id. + /// + /// The Id of the human to retrieve. + /// The human. + public Human GetHuman(string id) + { + return _repository.GetHuman(id); + } + + /// + /// Get a particular droid by Id. + /// + /// The Id of the droid. + /// The droid. + public Droid GetDroid(string id) + { + return _repository.GetDroid(id); + } + + public IEnumerable GetCharacter(string[] characterIds, IResolverContext context) + { + foreach (string characterId in characterIds) + { + ICharacter character = _repository.GetCharacter(characterId); + if (character == null) + { + context.ReportError( + "Could not resolve a charachter for the " + + $"character-id {characterId}."); + } + else + { + yield return character; + } + } + } + + public IEnumerable Search(string text) + { + return _repository.Search(text); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Resolvers/SharedResolvers.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Resolvers/SharedResolvers.cs new file mode 100644 index 0000000..3911604 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Resolvers/SharedResolvers.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using HotChocolate; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars.Resolvers +{ + public class SharedResolvers + { + public IEnumerable GetCharacter( + [Parent]ICharacter character, + [Service]CharacterRepository repository) + { + foreach (string friendId in character.Friends) + { + ICharacter friend = repository.GetCharacter(friendId); + if (friend != null) + { + yield return friend; + } + } + } + + public double GetHeight(Unit? unit, [Parent]ICharacter character) + => ConvertToUnit(character.Height, unit); + + public double GetLength(Unit? unit, [Parent]Starship starship) + => ConvertToUnit(starship.Length, unit); + + private double ConvertToUnit(double length, Unit? unit) + { + if (unit == Unit.Foot) + { + return length * 3.28084d; + } + return length; + } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Startup.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Startup.cs new file mode 100644 index 0000000..2bab6bb --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Startup.cs @@ -0,0 +1,50 @@ +using HotChocolate; +using HotChocolate.AspNetCore; +using HotChocolate.Execution; +using HotChocolate.Subscriptions; +using HotChocolate.PersistedQueries.FileSystem; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using StarWars.Data; +using StarWars.Types; + +namespace StarWars +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + // Add the custom services like repositories etc ... + services.AddSingleton(); + services.AddSingleton(); + + // Add in-memory event provider + services.AddInMemorySubscriptionProvider(); + + services.AddReadOnlyFileSystemQueryStorage("./queries"); + + // Add GraphQL Services + services.AddGraphQL(sp => SchemaBuilder.New() + .AddServices(sp) + .AddQueryType() + .AddMutationType() + .AddSubscriptionType() + .AddType() + .AddType() + .AddType() + .Create(), + builder => builder.UsePersistedQueryPipeline()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app + .UseWebSockets() + .UseGraphQL("/graphql"); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Subscription.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Subscription.cs new file mode 100644 index 0000000..06a88b2 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Subscription.cs @@ -0,0 +1,23 @@ +using System; +using HotChocolate.Subscriptions; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Subscription + { + private readonly ReviewRepository _repository; + + public Subscription(ReviewRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + public Review OnReview(Episode episode, IEventMessage message) + { + return (Review)message.Payload; + } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/CharacterType.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/CharacterType.cs new file mode 100644 index 0000000..b9c5c98 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/CharacterType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; + +namespace StarWars.Types +{ + public class CharacterType + : InterfaceType + { + protected override void Configure(IInterfaceTypeDescriptor descriptor) + { + descriptor.Name("Character"); + + descriptor.Field(f => f.Id) + .Type>(); + + descriptor.Field(f => f.Name) + .Type(); + + descriptor.Field(f => f.Friends) + .UsePaging(); + + descriptor.Field(f => f.AppearsIn) + .Type>(); + + descriptor.Field(f => f.Height) + .Type() + .Argument("unit", a => a.Type>()); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/DroidType.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/DroidType.cs new file mode 100644 index 0000000..c34fe9a --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/DroidType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class DroidType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.AppearsIn) + .Type>(); + + descriptor.Field(r => r.GetCharacter(default, default)) + .UsePaging() + .Name("friends"); + + descriptor.Field(t => t.GetHeight(default, default)) + .Type() + .Argument("unit", a => a.Type>()) + .Name("height"); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/EpisodeType.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/EpisodeType.cs new file mode 100644 index 0000000..bfb484d --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/EpisodeType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class EpisodeType + : EnumType + { + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/HumanType.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/HumanType.cs new file mode 100644 index 0000000..3ab1c6a --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/HumanType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class HumanType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.AppearsIn) + .Type>(); + + descriptor.Field(r => r.GetCharacter(default, default)) + .UsePaging() + .Name("friends"); + + descriptor.Field(t => t.GetHeight(default, default)) + .Type() + .Argument("unit", a => a.Type>()) + .Name("height"); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/MutationType.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/MutationType.cs new file mode 100644 index 0000000..71963b7 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/MutationType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Types; + +namespace StarWars.Types +{ + public class MutationType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.CreateReview(default, default, default)) + .Type>() + .Argument("episode", a => a.Type>()) + .Argument("review", a => a.Type>()); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/QueryType.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/QueryType.cs new file mode 100644 index 0000000..193cf89 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/QueryType.cs @@ -0,0 +1,22 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class QueryType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetHero(default)) + .Type() + .Argument("episode", a => a.DefaultValue(Episode.NewHope)); + + descriptor.Field(t => t.GetCharacter(default, default)) + .Type>>>(); + + descriptor.Field(t => t.Search(default)) + .Type>(); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/ReviewInputType.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/ReviewInputType.cs new file mode 100644 index 0000000..300fbe6 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/ReviewInputType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class ReviewInputType + : InputObjectType + { + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/ReviewType.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/ReviewType.cs new file mode 100644 index 0000000..be94b38 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/ReviewType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class ReviewType + : ObjectType + { + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/SearchResultType.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/SearchResultType.cs new file mode 100644 index 0000000..7d1bb88 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/SearchResultType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class SearchResultType + : UnionType + { + protected override void Configure(IUnionTypeDescriptor descriptor) + { + descriptor.Name("SearchResult"); + descriptor.Type>(); + descriptor.Type(); + descriptor.Type(); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/StarshipType.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/StarshipType.cs new file mode 100644 index 0000000..7ad6b10 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/StarshipType.cs @@ -0,0 +1,18 @@ +using HotChocolate.Types; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class StarshipType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.GetLength(default, default)); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/SubscriptionType.cs b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/SubscriptionType.cs new file mode 100644 index 0000000..ed872e5 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/Types/SubscriptionType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace StarWars.Types +{ + public class SubscriptionType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.OnReview(default, default)) + .Type>() + .Argument("episode", arg => arg.Type>()); + } + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/queries/foo b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/queries/foo new file mode 100644 index 0000000..985c3eb --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/StarWars/queries/foo @@ -0,0 +1,5 @@ +query foo { + hero(episode: NEWHOPE) { + name + } +} diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/copy.txt b/cluj_apexvox_2019/Introduction/5_persisted_queries/copy.txt new file mode 100644 index 0000000..1e02c6a --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/copy.txt @@ -0,0 +1,6 @@ + + +services.AddReadOnlyFileSystemQueryStorage("./queries"); + +, + b => b.UsePersistedQueryPipeline()) diff --git a/cluj_apexvox_2019/Introduction/5_persisted_queries/global.json b/cluj_apexvox_2019/Introduction/5_persisted_queries/global.json new file mode 100644 index 0000000..79422f0 --- /dev/null +++ b/cluj_apexvox_2019/Introduction/5_persisted_queries/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "3.0.100" + } +} diff --git a/cluj_apexvox_2019/Introduction/Introduction.pptx b/cluj_apexvox_2019/Introduction/Introduction.pptx new file mode 100644 index 0000000..bbb4dc1 Binary files /dev/null and b/cluj_apexvox_2019/Introduction/Introduction.pptx differ diff --git a/cluj_apexvox_2019/Stitching/5_stitching/.vscode/launch.json b/cluj_apexvox_2019/Stitching/5_stitching/.vscode/launch.json new file mode 100644 index 0000000..d883fe4 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/Gateway/bin/Debug/netcoreapp2.1/stitched.dll", + "args": [], + "cwd": "${workspaceFolder}/Gateway", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/cluj_apexvox_2019/Stitching/5_stitching/.vscode/tasks.json b/cluj_apexvox_2019/Stitching/5_stitching/.vscode/tasks.json new file mode 100644 index 0000000..31c32bd --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/ContractStorage.cs b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/ContractStorage.cs new file mode 100644 index 0000000..57295c7 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/ContractStorage.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Demo.Contracts +{ + public class ContractStorage + { + public List Contracts { get; } = new List + { + new LifeInsuranceContract + { + Id = "1", + CustomerId= "1", + Premium = 123456.11 + }, + new LifeInsuranceContract + { + Id = "2", + CustomerId= "1", + Premium = 456789.12 + }, + new LifeInsuranceContract + { + Id = "3", + CustomerId = "2", + Premium = 789.12 + }, + new SomeOtherContract + { + Id = "1", + CustomerId= "1", + ExpiryDate = new DateTime(2015, 2, 1, 0,0,0, DateTimeKind.Utc) + }, + new SomeOtherContract + { + Id = "2", + CustomerId= "2", + ExpiryDate = new DateTime(2015, 5, 1, 0,0,0, DateTimeKind.Utc) + }, + new SomeOtherContract + { + Id = "3", + CustomerId= "3", + ExpiryDate = new DateTime(2017, 1, 30, 0,0,0, DateTimeKind.Utc) + }, + new SomeOtherContract + { + Id = "4", + CustomerId= "3", + ExpiryDate = new DateTime(2020, 1, 1, 0,0,0, DateTimeKind.Utc) + } + }; + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/ContractType.cs b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/ContractType.cs new file mode 100644 index 0000000..ff5b38b --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/ContractType.cs @@ -0,0 +1,14 @@ +using HotChocolate.Types; + +namespace Demo.Contracts +{ + public class ContractType + : InterfaceType + { + protected override void Configure(IInterfaceTypeDescriptor descriptor) + { + descriptor.Name("Contract"); + descriptor.Field("id").Type>(); + } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/IContract.cs b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/IContract.cs new file mode 100644 index 0000000..64397c4 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/IContract.cs @@ -0,0 +1,9 @@ +namespace Demo.Contracts +{ + public interface IContract + { + string Id { get; } + + string CustomerId { get; } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/LifeInsuranceContract.cs b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/LifeInsuranceContract.cs new file mode 100644 index 0000000..93a577a --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/LifeInsuranceContract.cs @@ -0,0 +1,12 @@ +namespace Demo.Contracts +{ + public class LifeInsuranceContract + : IContract + { + public string Id { get; set; } + + public string CustomerId { get; set; } + + public double Premium { get; set; } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/LifeInsuranceContractType.cs b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/LifeInsuranceContractType.cs new file mode 100644 index 0000000..c629ecf --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/LifeInsuranceContractType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Types; + +namespace Demo.Contracts +{ + public class LifeInsuranceContractType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.CustomerId).Ignore(); + } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/Program.cs b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/Program.cs new file mode 100644 index 0000000..4ec4019 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Demo.Contracts +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseUrls("http://localhost:5051") + .UseStartup(); + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/Query.cs b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/Query.cs new file mode 100644 index 0000000..363b8f7 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/Query.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HotChocolate.Types.Relay; + +namespace Demo.Contracts +{ + public class Query + { + private readonly IdSerializer _idSerializer = new IdSerializer(); + private readonly ContractStorage _contractStorage; + + public Query(ContractStorage contractStorage) + { + _contractStorage = contractStorage + ?? throw new ArgumentNullException(nameof(contractStorage)); + } + + public IContract GetContract(string contractId) + { + IdValue value = _idSerializer.Deserialize(contractId); + + if (value.TypeName == nameof(LifeInsuranceContract)) + { + return _contractStorage.Contracts + .OfType() + .FirstOrDefault(t => t.Id.Equals(value.Value)); + } + else + { + return _contractStorage.Contracts + .OfType() + .FirstOrDefault(t => t.Id.Equals(value.Value)); + } + } + + public IEnumerable GetContracts(string customerId) + { + IdValue value = _idSerializer.Deserialize(customerId); + + return _contractStorage.Contracts + .Where(t => t.CustomerId.Equals(value.Value)); + } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/QueryType.cs b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/QueryType.cs new file mode 100644 index 0000000..c012283 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/QueryType.cs @@ -0,0 +1,20 @@ +using HotChocolate.Types; + +namespace Demo.Contracts +{ + public class QueryType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetContract(default)) + .Argument("contractId", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.GetContracts(default)) + .Argument("customerId", a => a.Type>()) + .Type>>(); + } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/SomeOtherContract.cs b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/SomeOtherContract.cs new file mode 100644 index 0000000..54c1572 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/SomeOtherContract.cs @@ -0,0 +1,14 @@ +using System; + +namespace Demo.Contracts +{ + public class SomeOtherContract + : IContract + { + public string Id { get; set; } + + public string CustomerId { get; set; } + + public DateTime ExpiryDate { get; set; } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/SomeOtherContractType.cs b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/SomeOtherContractType.cs new file mode 100644 index 0000000..303d676 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/SomeOtherContractType.cs @@ -0,0 +1,23 @@ +using HotChocolate.Types; + +namespace Demo.Contracts +{ + public class SomeOtherContractType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.CustomerId) + .Ignore(); + + descriptor.Field(t => t.ExpiryDate) + .Type>(); + } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/Startup.cs b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/Startup.cs new file mode 100644 index 0000000..71ea528 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/Startup.cs @@ -0,0 +1,39 @@ +using HotChocolate; +using HotChocolate.AspNetCore; +using Demo.Contracts; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Demo.Contracts +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + + // Add GraphQL Services + services.AddGraphQL(Schema.Create(c => + { + c.RegisterQueryType(); + c.RegisterType(); + c.RegisterType(); + + c.UseGlobalObjectIdentifier(); + })); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(); + app.UsePlayground(); + } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/contract.csproj b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/contract.csproj new file mode 100644 index 0000000..c36ea18 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/ContractSchema/contract.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp2.1 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + + + \ No newline at end of file diff --git a/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/Consultant.cs b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/Consultant.cs new file mode 100644 index 0000000..903e6c5 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/Consultant.cs @@ -0,0 +1,9 @@ +namespace Demo.Customers +{ + public class Consultant + : ICustomerOrConsultant + { + public string Id { get; set; } + public string Name { get; set; } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/ConsultantType.cs b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/ConsultantType.cs new file mode 100644 index 0000000..d3d3e94 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/ConsultantType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace Demo.Customers +{ + public class ConsultantType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Name).Type>(); + } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/Customer.cs b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/Customer.cs new file mode 100644 index 0000000..a0c5abc --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/Customer.cs @@ -0,0 +1,10 @@ +namespace Demo.Customers +{ + public class Customer + : ICustomerOrConsultant + { + public string Id { get; set; } + public string Name { get; set; } + public string ConsultantId { get; set; } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/CustomerOrConsultantType.cs b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/CustomerOrConsultantType.cs new file mode 100644 index 0000000..e6aed53 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/CustomerOrConsultantType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace Demo.Customers +{ + public class CustomerOrConsultantType + : UnionType + { + protected override void Configure(IUnionTypeDescriptor descriptor) + { + descriptor.Name("CustomerOrConsultant"); + descriptor.Type(); + descriptor.Type(); + } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/CustomerRepository.cs b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/CustomerRepository.cs new file mode 100644 index 0000000..0b11ec7 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/CustomerRepository.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace Demo.Customers +{ + public class CustomerRepository + { + public List Customers { get; } = new List + { + new Customer + { + Id = "1", + Name = "Freddy Freeman", + ConsultantId = "1" + }, + new Customer + { + Id = "2", + Name = "Carol Danvers", + ConsultantId = "1" + }, + new Customer + { + Id = "3", + Name = "Walter Lawson", + ConsultantId = "2" + } + }; + + public List Consultants { get; } = new List + { + new Consultant + { + Id = "1", + Name = "Jordan Belfort", + }, + new Consultant + { + Id = "2", + Name = "Gordon Gekko", + } + }; + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/CustomerResolver.cs b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/CustomerResolver.cs new file mode 100644 index 0000000..f30784e --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/CustomerResolver.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq; +using HotChocolate; + +namespace Demo.Customers +{ + public class CustomerResolver + { + public Consultant GetConsultant( + Customer customer, + [Service]CustomerRepository repository) + { + if (customer.ConsultantId != null) + { + return repository.Consultants.FirstOrDefault( + t => t.Id.Equals(customer.ConsultantId)); + } + return null; + } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/CustomerType.cs b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/CustomerType.cs new file mode 100644 index 0000000..79e2d17 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/CustomerType.cs @@ -0,0 +1,20 @@ +using HotChocolate.Types; + +namespace Demo.Customers +{ + public class CustomerType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Name).Type>(); + descriptor.Field(t => t.ConsultantId).Ignore(); + + descriptor.Field( + t => t.GetConsultant(default, default)) + .Type(); + } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/ICustomerOrConsultant.cs b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/ICustomerOrConsultant.cs new file mode 100644 index 0000000..17abcb0 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/ICustomerOrConsultant.cs @@ -0,0 +1,7 @@ +namespace Demo.Customers +{ + public interface ICustomerOrConsultant + { + + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/Program.cs b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/Program.cs new file mode 100644 index 0000000..15df5b0 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Demo.Customers +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseUrls("http://localhost:5050") + .UseStartup(); + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/Query.cs b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/Query.cs new file mode 100644 index 0000000..f4a25e9 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/Query.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HotChocolate.Types.Relay; + +namespace Demo.Customers +{ + public class Query + { + private readonly IdSerializer _idSerializer = new IdSerializer(); + private readonly CustomerRepository _repository; + + public Query(CustomerRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + public Customer GetCustomer(string id) + { + IdValue value = _idSerializer.Deserialize(id); + return _repository.Customers + .FirstOrDefault(t => t.Id.Equals(value.Value)); + } + + public IEnumerable GetCustomers() + { + return _repository.Customers; + } + + public Consultant GetConsultant(string id) + { + IdValue value = _idSerializer.Deserialize(id); + return _repository.Consultants + .FirstOrDefault(t => t.Id.Equals(value.Value)); + } + + public ICustomerOrConsultant GetCustomerOrConsultant(string id) + { + IdValue value = _idSerializer.Deserialize(id); + if (value.TypeName == "Consultant") + { + return GetConsultant(id); + } + return GetCustomer(id); + } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/QueryType.cs b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/QueryType.cs new file mode 100644 index 0000000..183bae0 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/QueryType.cs @@ -0,0 +1,27 @@ +using HotChocolate.Types; + +namespace Demo.Customers +{ + public class QueryType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetCustomer(default)) + .Argument("id", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.GetCustomers()) + .Type>>>(); + + descriptor.Field(t => t.GetConsultant(default)) + .Argument("id", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.GetCustomerOrConsultant(default)) + .Argument("id", a => a.Type>()) + .Type(); + } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/Startup.cs b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/Startup.cs new file mode 100644 index 0000000..4bcace5 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/Startup.cs @@ -0,0 +1,35 @@ +using HotChocolate; +using HotChocolate.AspNetCore; +using Demo.Customers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Demo.Customers +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + + // Add GraphQL Services + services.AddGraphQL(Schema.Create(c => + { + c.RegisterQueryType(); + c.UseGlobalObjectIdentifier(); + })); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(); + app.UsePlayground(); + } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/customer.csproj b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/customer.csproj new file mode 100644 index 0000000..c36ea18 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/CustomerSchema/customer.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp2.1 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + + + \ No newline at end of file diff --git a/cluj_apexvox_2019/Stitching/5_stitching/Gateway/.vscode/launch.json b/cluj_apexvox_2019/Stitching/5_stitching/Gateway/.vscode/launch.json new file mode 100644 index 0000000..2ed183d --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/Gateway/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.1/stitched.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart", + "launchBrowser": { + "enabled": true, + "args": "${auto-detect-url}", + "windows": { + "command": "cmd.exe", + "args": "/C start ${auto-detect-url}" + }, + "osx": { + "command": "open" + }, + "linux": { + "command": "xdg-open" + } + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/cluj_apexvox_2019/Stitching/5_stitching/Gateway/.vscode/tasks.json b/cluj_apexvox_2019/Stitching/5_stitching/Gateway/.vscode/tasks.json new file mode 100644 index 0000000..a914fef --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/Gateway/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet build", + "type": "shell", + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/cluj_apexvox_2019/Stitching/5_stitching/Gateway/Extensions.graphql b/cluj_apexvox_2019/Stitching/5_stitching/Gateway/Extensions.graphql new file mode 100644 index 0000000..e69de29 diff --git a/cluj_apexvox_2019/Stitching/5_stitching/Gateway/Program.cs b/cluj_apexvox_2019/Stitching/5_stitching/Gateway/Program.cs new file mode 100644 index 0000000..f629620 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/Gateway/Program.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace Demo.Stitching +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/Gateway/Startup.cs b/cluj_apexvox_2019/Stitching/5_stitching/Gateway/Startup.cs new file mode 100644 index 0000000..308d489 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/Gateway/Startup.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.AspNetCore; +using HotChocolate.Execution; +using HotChocolate.Resolvers; +using HotChocolate.Stitching; +using HotChocolate.Types; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Demo.Stitching +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + // Setup the clients that shall be used to access the remote endpoints. + services.AddHttpClient("customer", (sp, client) => + { + // in order to pass on the token or any other headers to the backend schema use the IHttpContextAccessor + HttpContext context = sp.GetRequiredService().HttpContext; + client.BaseAddress = new Uri("http://127.0.0.1:5050"); + }); + + services.AddHttpClient("contract", (sp, client) => + { + // in order to pass on the token or any other headers to the backend schema use the IHttpContextAccessor + HttpContext context = sp.GetRequiredService().HttpContext; + client.BaseAddress = new Uri("http://127.0.0.1:5051"); + }); + + services.AddHttpContextAccessor(); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(new QueryMiddlewareOptions { EnableSubscriptions = false }); + app.UsePlayground(); + } + } +} diff --git a/cluj_apexvox_2019/Stitching/5_stitching/Gateway/stitched.csproj b/cluj_apexvox_2019/Stitching/5_stitching/Gateway/stitched.csproj new file mode 100644 index 0000000..b8aef24 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/Gateway/stitched.csproj @@ -0,0 +1,35 @@ + + + + netcoreapp2.1 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + + + + + + + Always + + + + diff --git a/cluj_apexvox_2019/Stitching/5_stitching/README.md b/cluj_apexvox_2019/Stitching/5_stitching/README.md new file mode 100644 index 0000000..d807a0c --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/README.md @@ -0,0 +1,20 @@ +# Schema Stitching Example + +This example shows how you can implement as stitched schema with Hot Chocolate. + +This example consists of the following projects: + +- CustomerSchema + The customer schema contains a GraphQL server that serves up a schema around a customer entity. + +- ContractSchema + The contract schema contains a GraphQL server that serves up a schema that provides insurance contract entities that can be assoicated with customers. + +- Stitching + The stitching project contains a GraphQL server that stitches the former mentiond GraphQL schemas together. + +1. Start the customer and contract servers with `dotnet run` +2. When the former servers are running start the stitching server with `dotnet run` +3. Head over to `http://127.0.0.1/playground` and test out some queries. + +[Hot Chocolate Documentation](https://hotchocolate.io) diff --git a/cluj_apexvox_2019/Stitching/5_stitching/Stitching.sln.sln b/cluj_apexvox_2019/Stitching/5_stitching/Stitching.sln.sln new file mode 100644 index 0000000..ca605b6 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/Stitching.sln.sln @@ -0,0 +1,62 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "contract", "ContractSchema\contract.csproj", "{09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "customer", "CustomerSchema\customer.csproj", "{C538BC50-D306-4AE3-8FE4-38481C27427D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "stitched", "Gateway\stitched.csproj", "{E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|x64.ActiveCfg = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|x64.Build.0 = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|x86.ActiveCfg = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|x86.Build.0 = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|Any CPU.Build.0 = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|x64.ActiveCfg = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|x64.Build.0 = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|x86.ActiveCfg = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|x86.Build.0 = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|x64.ActiveCfg = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|x64.Build.0 = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|x86.Build.0 = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|Any CPU.Build.0 = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|x64.ActiveCfg = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|x64.Build.0 = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|x86.ActiveCfg = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|x86.Build.0 = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|x64.ActiveCfg = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|x64.Build.0 = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|x86.ActiveCfg = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|x86.Build.0 = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|Any CPU.Build.0 = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|x64.ActiveCfg = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|x64.Build.0 = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|x86.ActiveCfg = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/cluj_apexvox_2019/Stitching/5_stitching/copy.txt b/cluj_apexvox_2019/Stitching/5_stitching/copy.txt new file mode 100644 index 0000000..3d6f935 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/5_stitching/copy.txt @@ -0,0 +1,60 @@ +services.AddStitchedSchema(builder => builder + .AddSchemaFromHttp("customer") + .AddSchemaFromHttp("contract") + .AddExtensionsFromFile("./Extensions.graphql") + .AddSchemaConfiguration(c => + { + + })); + +// .IgnoreRootTypes() + .RenameType("LifeInsuranceContract", "LifeInsurance") + +extend type Query { + me: Customer + @delegate( + schema: "customer" + path: "customer(id:$contextData:currentUserId)" + ) +} + +extend type Customer { + contracts: [Contract!] + @delegate(schema: "contract", path: "contracts(customerId:$fields:id)") +} + +services.AddQueryRequestInterceptor((context, builder, cancellationToken) => +{ + builder.AddProperty("currentUserId", "Q3VzdG9tZXIKZDE="); + return Task.CompletedTask; +}); + +c.RegisterType(); +// custom resolver that depends on data from a remote schema. +c.Map(new FieldReference("Customer", "foo"), next => context => +{ + OrderedDictionary obj = context.Parent(); + context.Result = obj["name"] + "_" + obj["id"]; + return Task.CompletedTask; +}); + + +public class SomeOtherContractExtension + : ObjectTypeExtension + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name("SomeOtherContract"); + descriptor.Field("expiresInDays") + .Type>() + .Directive(new ComputedDirective { DependantOn = new NameString[] { "expiryDate" } }) + .Resolver(context => + { + var obj = context.Parent>(); + var serializedExpiryDate = obj["expiryDate"]; + var dateType = (ISerializableType)context.ObjectType.Fields["expiryDate"].Type; + var offset = (DateTimeOffset)dateType.Deserialize(serializedExpiryDate); + return offset.DateTime.Subtract(DateTime.UtcNow).Days; + }); + } + } diff --git a/cluj_apexvox_2019/Stitching/6_testing/.vscode/launch.json b/cluj_apexvox_2019/Stitching/6_testing/.vscode/launch.json new file mode 100644 index 0000000..cc09e8b --- /dev/null +++ b/cluj_apexvox_2019/Stitching/6_testing/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/netcoreapp3.0/Testing.dll", + "args": [], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/cluj_apexvox_2019/Stitching/6_testing/.vscode/tasks.json b/cluj_apexvox_2019/Stitching/6_testing/.vscode/tasks.json new file mode 100644 index 0000000..31c32bd --- /dev/null +++ b/cluj_apexvox_2019/Stitching/6_testing/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/cluj_apexvox_2019/Stitching/6_testing/Testing.csproj b/cluj_apexvox_2019/Stitching/6_testing/Testing.csproj new file mode 100644 index 0000000..43d1d5f --- /dev/null +++ b/cluj_apexvox_2019/Stitching/6_testing/Testing.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.0 + _9_testing + + false + + + + + + + + + + + + diff --git a/cluj_apexvox_2019/Stitching/6_testing/UnitTest1.cs b/cluj_apexvox_2019/Stitching/6_testing/UnitTest1.cs new file mode 100644 index 0000000..e3df79c --- /dev/null +++ b/cluj_apexvox_2019/Stitching/6_testing/UnitTest1.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Execution; +using HotChocolate.Types; +using Snapshooter.Xunit; +using Xunit; + +namespace Testing +{ + public class MyTests + { + + } +} diff --git a/cluj_apexvox_2019/Stitching/6_testing/copy.txt b/cluj_apexvox_2019/Stitching/6_testing/copy.txt new file mode 100644 index 0000000..a075329 --- /dev/null +++ b/cluj_apexvox_2019/Stitching/6_testing/copy.txt @@ -0,0 +1,40 @@ +[Fact] +public void Schema_Snapshot() +{ + // arrange + // act + ISchema schema = SchemaBuilder.New() + .AddQueryType(d => d + .Name("Query") + .Field("foo") + .Resolver("bar")) + .Create(); + + // assert + schema.ToString().MatchSnapshot(); +} + +[Fact] +public async Task Schema_Integration_Test() +{ + // arrange + ISchema schema = SchemaBuilder.New() + .AddQueryType(d => + { + d.Name("Query"); + d.Field("foo").Resolver("bar"); + d.Field("baz").Resolver(DateTimeOffset.UtcNow); + }) + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + // act + IExecutionResult result = await executor.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ foo baz }") + .Create()); + + // assert + result.MatchSnapshot(matchOptions => matchOptions.IgnoreField("Data.baz")); +} diff --git a/cluj_apexvox_2019/Stitching/Stitching.pptx b/cluj_apexvox_2019/Stitching/Stitching.pptx new file mode 100644 index 0000000..ba2605e Binary files /dev/null and b/cluj_apexvox_2019/Stitching/Stitching.pptx differ diff --git a/kiev_dotnetfest_2019/Demos/1_graphql_vs_rest/index.html b/kiev_dotnetfest_2019/Demos/1_graphql_vs_rest/index.html new file mode 100644 index 0000000..889c94b --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/1_graphql_vs_rest/index.html @@ -0,0 +1,268 @@ + + + + + GraphQL vs. REST + + + +
+ + vs. + +
+
+ Loading... +
+
+

+ An unsorted list of characters who played in the same films where Luke + Skywalker were present +

+

+ Used and returned + characters which took + ms. +

+
    +
    + + + diff --git a/kiev_dotnetfest_2019/Demos/2_hello_world/.vscode/launch.json b/kiev_dotnetfest_2019/Demos/2_hello_world/.vscode/launch.json new file mode 100644 index 0000000..c509473 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/2_hello_world/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp3.0/2_hello_world.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/kiev_dotnetfest_2019/Demos/2_hello_world/.vscode/tasks.json b/kiev_dotnetfest_2019/Demos/2_hello_world/.vscode/tasks.json new file mode 100644 index 0000000..31c32bd --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/2_hello_world/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/kiev_dotnetfest_2019/Demos/2_hello_world/HelloWorld.csproj b/kiev_dotnetfest_2019/Demos/2_hello_world/HelloWorld.csproj new file mode 100644 index 0000000..b3d059f --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/2_hello_world/HelloWorld.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.0 + _2_hello_world + + + + + + + diff --git a/kiev_dotnetfest_2019/Demos/2_hello_world/Program.cs b/kiev_dotnetfest_2019/Demos/2_hello_world/Program.cs new file mode 100644 index 0000000..fd016d1 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/2_hello_world/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace HelloWorld +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/kiev_dotnetfest_2019/Demos/2_hello_world/Query.cs b/kiev_dotnetfest_2019/Demos/2_hello_world/Query.cs new file mode 100644 index 0000000..b36653b --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/2_hello_world/Query.cs @@ -0,0 +1,9 @@ +namespace HelloWorld +{ + public class Query + { + public string GetHello() => "World"; + + public string GetGreetings(string text) => text; + } +} diff --git a/kiev_dotnetfest_2019/Demos/2_hello_world/QueryType.cs b/kiev_dotnetfest_2019/Demos/2_hello_world/QueryType.cs new file mode 100644 index 0000000..4fdbedc --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/2_hello_world/QueryType.cs @@ -0,0 +1,13 @@ +using HotChocolate.Types; + +namespace HelloWorld +{ + public class QueryType : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetHello()).Type>(); + // Note: GetGreetings is inferred + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/2_hello_world/Startup.cs b/kiev_dotnetfest_2019/Demos/2_hello_world/Startup.cs new file mode 100644 index 0000000..56a9a4b --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/2_hello_world/Startup.cs @@ -0,0 +1,24 @@ +using HotChocolate; +using HotChocolate.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace HelloWorld +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddGraphQL(sp => SchemaBuilder.New() + .AddQueryType() + .AddServices(sp) + .Create()); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseGraphQL(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/2_hello_world/appsettings.Development.json b/kiev_dotnetfest_2019/Demos/2_hello_world/appsettings.Development.json new file mode 100644 index 0000000..a2880cb --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/2_hello_world/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/2_hello_world/appsettings.json b/kiev_dotnetfest_2019/Demos/2_hello_world/appsettings.json new file mode 100644 index 0000000..81ff877 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/2_hello_world/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/kiev_dotnetfest_2019/Demos/2_hello_world/global.json b/kiev_dotnetfest_2019/Demos/2_hello_world/global.json new file mode 100644 index 0000000..79422f0 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/2_hello_world/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "3.0.100" + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/.vscode/launch.json b/kiev_dotnetfest_2019/Demos/3_operations/.vscode/launch.json new file mode 100644 index 0000000..d3f889e --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/StarWars/bin/Debug/netcoreapp3.0/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/kiev_dotnetfest_2019/Demos/3_operations/.vscode/tasks.json b/kiev_dotnetfest_2019/Demos/3_operations/.vscode/tasks.json new file mode 100644 index 0000000..383f2db --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/.vscode/tasks.json @@ -0,0 +1,25 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + "StarWars", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/.vscode/launch.json b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/.vscode/launch.json new file mode 100644 index 0000000..401f807 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.1/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/.vscode/tasks.json b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/.vscode/tasks.json new file mode 100644 index 0000000..68ae5ff --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet build", + "type": "shell", + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Data/CharacterRepository.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Data/CharacterRepository.cs new file mode 100644 index 0000000..dd09134 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Data/CharacterRepository.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StarWars.Models; + +namespace StarWars.Data +{ + public class CharacterRepository + { + private Dictionary _characters; + private Dictionary _starships; + + public CharacterRepository() + { + _characters = CreateCharacters().ToDictionary(t => t.Id); + _starships = CreateStarships().ToDictionary(t => t.Id); + } + + public ICharacter GetHero(Episode episode) + { + if (episode == Episode.Empire) + { + return _characters["1000"]; + } + return _characters["2001"]; + } + + public ICharacter GetCharacter(string id) + { + if (_characters.TryGetValue(id, out ICharacter c)) + { + return c; + } + return null; + } + + public Human GetHuman(string id) + { + if (_characters.TryGetValue(id, out ICharacter c) + && c is Human h) + { + return h; + } + return null; + } + + public Droid GetDroid(string id) + { + if (_characters.TryGetValue(id, out ICharacter c) + && c is Droid d) + { + return d; + } + return null; + } + + public IEnumerable Search(string text) + { +#if ASPNETCLASSIC + IEnumerable filteredCharacters = _characters.Values + .Where(t => t.Name.Contains(text)); +#else + IEnumerable filteredCharacters = _characters.Values + .Where(t => t.Name.Contains(text, + StringComparison.OrdinalIgnoreCase)); +#endif + + foreach (ICharacter character in filteredCharacters) + { + yield return character; + } + +#if ASPNETCLASSIC + IEnumerable filteredStarships = _starships.Values + .Where(t => t.Name.Contains(text)); +#else + IEnumerable filteredStarships = _starships.Values + .Where(t => t.Name.Contains(text, + StringComparison.OrdinalIgnoreCase)); +#endif + + foreach (Starship starship in filteredStarships) + { + yield return starship; + } + } + + private static IEnumerable CreateCharacters() + { + yield return new Human + { + Id = "1000", + Name = "Luke Skywalker", + Friends = new[] { "1002", "1003", "2000", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Tatooine" + }; + + yield return new Human + { + Id = "1001", + Name = "Darth Vader", + Friends = new[] { "1004" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Tatooine" + }; + + yield return new Human + { + Id = "1002", + Name = "Han Solo", + Friends = new[] { "1000", "1003", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi } + }; + + yield return new Human + { + Id = "1003", + Name = "Leia Organa", + Friends = new[] { "1000", "1002", "2000", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Alderaan" + }; + + yield return new Human + { + Id = "1004", + Name = "Wilhuff Tarkin", + Friends = new[] { "1001" }, + AppearsIn = new[] { Episode.NewHope } + }; + + yield return new Droid + { + Id = "2000", + Name = "C-3PO", + Friends = new[] { "1000", "1002", "1003", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + PrimaryFunction = "Protocol" + }; + + yield return new Droid + { + Id = "2001", + Name = "R2-D2", + Friends = new[] { "1000", "1002", "1003" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + PrimaryFunction = "Astromech" + }; + } + + private static IEnumerable CreateStarships() + { + yield return new Starship + { + Id = "3000", + Name = "TIE Advanced x1", + Length = 9.2 + }; + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Data/ReviewRepository.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Data/ReviewRepository.cs new file mode 100644 index 0000000..a586306 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Data/ReviewRepository.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using StarWars.Models; + +namespace StarWars.Data +{ + public class ReviewRepository + { + private readonly Dictionary> _data = + new Dictionary>(); + + public void AddReview(Episode episode, Review review) + { + if (!_data.TryGetValue(episode, out List reviews)) + { + reviews = new List(); + _data[episode] = reviews; + } + + reviews.Add(review); + } + + public IEnumerable GetReviews(Episode episode) + { + if (_data.TryGetValue(episode, out List reviews)) + { + return reviews; + } + return Array.Empty(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Droid.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Droid.cs new file mode 100644 index 0000000..0ee4213 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Droid.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A droid in the Star Wars universe. + /// + public class Droid + : ICharacter + { + /// + public string Id { get; set; } + + /// + public string Name { get; set; } + + /// + public IReadOnlyList Friends { get; set; } + + /// + public IReadOnlyList AppearsIn { get; set; } + + /// + /// The droid's primary function. + /// + public string PrimaryFunction { get; set; } + + /// + public double Height { get; } = 1.72d; + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Episode.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Episode.cs new file mode 100644 index 0000000..6900cf6 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Episode.cs @@ -0,0 +1,21 @@ +namespace StarWars.Models +{ + /// + /// The Star Wars episodes. + /// + public enum Episode + { + /// + /// Star Wars Episode IV: A New Hope + /// + NewHope, + /// + /// Star Wars Episode V: Empire Strikes Back + /// + Empire, + /// + /// Star Wars Episode VI: Return of the Jedi + /// + Jedi + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Human.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Human.cs new file mode 100644 index 0000000..caf6021 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Human.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A human character in the Star Wars universe. + /// + public class Human + : ICharacter + { + /// + public string Id { get; set; } + + /// + public string Name { get; set; } + + /// + public IReadOnlyList Friends { get; set; } + + /// + public IReadOnlyList AppearsIn { get; set; } + + /// + /// The planet the character is originally from. + /// + public string HomePlanet { get; set; } + + /// + public double Height { get; } = 1.72d; + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/ICharacter.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/ICharacter.cs new file mode 100644 index 0000000..f9186c1 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/ICharacter.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A character in the Star Wars universe. + /// + public interface ICharacter + { + /// + /// The unique identifier for the character. + /// + string Id { get; } + + /// + /// The name of the character. + /// + string Name { get; } + + /// + /// The names of the character's friends. + /// + IReadOnlyList Friends { get; } + + /// + /// The episodes the character appears in. + /// + IReadOnlyList AppearsIn { get; } + + /// + /// The height of the character. + /// + double Height { get; } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Review.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Review.cs new file mode 100644 index 0000000..3f18f16 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Review.cs @@ -0,0 +1,18 @@ +namespace StarWars.Models +{ + /// + /// A review of a particular movie. + /// + public class Review + { + /// + /// The number of stars given for this review. + /// + public int Stars { get; set; } + + /// + /// An explanation for the rating. + /// + public string Commentary { get; set; } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Starship.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Starship.cs new file mode 100644 index 0000000..5d7c241 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Starship.cs @@ -0,0 +1,23 @@ +namespace StarWars.Models +{ + /// + /// A starship in the Star Wars universe. + /// + public class Starship + { + /// + /// The Id of the starship. + /// + public string Id { get; set; } + + /// + /// The name of the starship. + /// + public string Name { get; set; } + + /// + /// The length of the starship. + /// + public double Length { get; set; } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Unit.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Unit.cs new file mode 100644 index 0000000..15316aa --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Models/Unit.cs @@ -0,0 +1,11 @@ +namespace StarWars.Models +{ + /// + /// Different units of measurement. + /// + public enum Unit + { + Foot, + Meters + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Mutation.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Mutation.cs new file mode 100644 index 0000000..45d4adc --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Mutation.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Subscriptions; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Mutation + { + private readonly ReviewRepository _repository; + + public Mutation(ReviewRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Creates a review for a given Star Wars episode. + /// + /// The episode to review. + /// The review. + /// The event sending service. + /// The created review. + public async Task CreateReview( + Episode episode, Review review, + [Service]IEventSender eventSender) + { + _repository.AddReview(episode, review); + await eventSender.SendAsync(new OnReviewMessage(episode, review)); + return review; + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/OnReviewMessage.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/OnReviewMessage.cs new file mode 100644 index 0000000..95f9cff --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/OnReviewMessage.cs @@ -0,0 +1,23 @@ +using HotChocolate.Language; +using HotChocolate.Subscriptions; +using StarWars.Models; + +namespace StarWars +{ + public class OnReviewMessage + : EventMessage + { + public OnReviewMessage(Episode episode, Review review) + : base(CreateEventDescription(episode), review) + { + } + + private static EventDescription CreateEventDescription(Episode episode) + { + return new EventDescription("onReview", + new ArgumentNode("episode", + new EnumValueNode( + episode.ToString().ToUpperInvariant()))); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Program.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Program.cs new file mode 100644 index 0000000..0358799 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace StarWars +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Query.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Query.cs new file mode 100644 index 0000000..887534e --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Query.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using HotChocolate.Resolvers; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Query + { + private readonly CharacterRepository _repository; + + public Query(CharacterRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Retrieve a hero by a particular Star Wars episode. + /// + /// The episode to look up by. + /// The character. + public ICharacter GetHero(Episode episode) + { + return _repository.GetHero(episode); + } + + /// + /// Gets a human by Id. + /// + /// The Id of the human to retrieve. + /// The human. + public Human GetHuman(string id) + { + return _repository.GetHuman(id); + } + + /// + /// Get a particular droid by Id. + /// + /// The Id of the droid. + /// The droid. + public Droid GetDroid(string id) + { + return _repository.GetDroid(id); + } + + public IEnumerable GetCharacter(string[] characterIds, IResolverContext context) + { + foreach (string characterId in characterIds) + { + ICharacter character = _repository.GetCharacter(characterId); + if (character == null) + { + context.ReportError( + "Could not resolve a charachter for the " + + $"character-id {characterId}."); + } + else + { + yield return character; + } + } + } + + public IEnumerable Search(string text) + { + return _repository.Search(text); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Resolvers/SharedResolvers.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Resolvers/SharedResolvers.cs new file mode 100644 index 0000000..3911604 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Resolvers/SharedResolvers.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using HotChocolate; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars.Resolvers +{ + public class SharedResolvers + { + public IEnumerable GetCharacter( + [Parent]ICharacter character, + [Service]CharacterRepository repository) + { + foreach (string friendId in character.Friends) + { + ICharacter friend = repository.GetCharacter(friendId); + if (friend != null) + { + yield return friend; + } + } + } + + public double GetHeight(Unit? unit, [Parent]ICharacter character) + => ConvertToUnit(character.Height, unit); + + public double GetLength(Unit? unit, [Parent]Starship starship) + => ConvertToUnit(starship.Length, unit); + + private double ConvertToUnit(double length, Unit? unit) + { + if (unit == Unit.Foot) + { + return length * 3.28084d; + } + return length; + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/StarWars.csproj b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/StarWars.csproj new file mode 100644 index 0000000..1b041de --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/StarWars.csproj @@ -0,0 +1,34 @@ + + + + netcoreapp3.0 + false + 7.2 + true + $(NoWarn);1591 + + + + portable + true + + + + pdbonly + true + + + + + + + + + + + + + + + + diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Startup.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Startup.cs new file mode 100644 index 0000000..b55e4d8 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Startup.cs @@ -0,0 +1,66 @@ +using System.Security.Claims; +using HotChocolate; +using HotChocolate.AspNetCore; +using HotChocolate.AspNetCore.Voyager; +using HotChocolate.Execution.Configuration; +using HotChocolate.Subscriptions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using StarWars.Data; +using StarWars.Types; + +namespace StarWars +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + // Add the custom services like repositories etc ... + services.AddSingleton(); + services.AddSingleton(); + + // Add in-memory event provider + services.AddInMemorySubscriptionProvider(); + + // Add GraphQL Services + services.AddGraphQL(sp => SchemaBuilder.New() + .AddServices(sp) + + // Adds the authorize directive and + // enable the authorization middleware. + .AddAuthorizeDirectiveType() + + .AddQueryType() + .AddMutationType() + .AddSubscriptionType() + .AddType() + .AddType() + .AddType() + .Create()); + + + // Add Authorization Policy + services.AddAuthorization(options => + { + options.AddPolicy("HasCountry", policy => + policy.RequireAssertion(context => + context.User.HasClaim(c => + (c.Type == ClaimTypes.Country)))); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app + .UseWebSockets() + .UseGraphQL("/graphql") + .UseGraphiQL("/graphql") + .UsePlayground("/graphql") + .UseVoyager("/graphql"); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Subscription.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Subscription.cs new file mode 100644 index 0000000..06a88b2 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Subscription.cs @@ -0,0 +1,23 @@ +using System; +using HotChocolate.Subscriptions; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Subscription + { + private readonly ReviewRepository _repository; + + public Subscription(ReviewRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + public Review OnReview(Episode episode, IEventMessage message) + { + return (Review)message.Payload; + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/CharacterType.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/CharacterType.cs new file mode 100644 index 0000000..b9c5c98 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/CharacterType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; + +namespace StarWars.Types +{ + public class CharacterType + : InterfaceType + { + protected override void Configure(IInterfaceTypeDescriptor descriptor) + { + descriptor.Name("Character"); + + descriptor.Field(f => f.Id) + .Type>(); + + descriptor.Field(f => f.Name) + .Type(); + + descriptor.Field(f => f.Friends) + .UsePaging(); + + descriptor.Field(f => f.AppearsIn) + .Type>(); + + descriptor.Field(f => f.Height) + .Type() + .Argument("unit", a => a.Type>()); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/DroidType.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/DroidType.cs new file mode 100644 index 0000000..c34fe9a --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/DroidType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class DroidType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.AppearsIn) + .Type>(); + + descriptor.Field(r => r.GetCharacter(default, default)) + .UsePaging() + .Name("friends"); + + descriptor.Field(t => t.GetHeight(default, default)) + .Type() + .Argument("unit", a => a.Type>()) + .Name("height"); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/EpisodeType.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/EpisodeType.cs new file mode 100644 index 0000000..bfb484d --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/EpisodeType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class EpisodeType + : EnumType + { + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/HumanType.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/HumanType.cs new file mode 100644 index 0000000..3ab1c6a --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/HumanType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class HumanType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.AppearsIn) + .Type>(); + + descriptor.Field(r => r.GetCharacter(default, default)) + .UsePaging() + .Name("friends"); + + descriptor.Field(t => t.GetHeight(default, default)) + .Type() + .Argument("unit", a => a.Type>()) + .Name("height"); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/MutationType.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/MutationType.cs new file mode 100644 index 0000000..71963b7 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/MutationType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Types; + +namespace StarWars.Types +{ + public class MutationType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.CreateReview(default, default, default)) + .Type>() + .Argument("episode", a => a.Type>()) + .Argument("review", a => a.Type>()); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/QueryType.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/QueryType.cs new file mode 100644 index 0000000..193cf89 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/QueryType.cs @@ -0,0 +1,22 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class QueryType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetHero(default)) + .Type() + .Argument("episode", a => a.DefaultValue(Episode.NewHope)); + + descriptor.Field(t => t.GetCharacter(default, default)) + .Type>>>(); + + descriptor.Field(t => t.Search(default)) + .Type>(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/ReviewInputType.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/ReviewInputType.cs new file mode 100644 index 0000000..300fbe6 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/ReviewInputType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class ReviewInputType + : InputObjectType + { + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/ReviewType.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/ReviewType.cs new file mode 100644 index 0000000..be94b38 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/ReviewType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class ReviewType + : ObjectType + { + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/SearchResultType.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/SearchResultType.cs new file mode 100644 index 0000000..7d1bb88 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/SearchResultType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class SearchResultType + : UnionType + { + protected override void Configure(IUnionTypeDescriptor descriptor) + { + descriptor.Name("SearchResult"); + descriptor.Type>(); + descriptor.Type(); + descriptor.Type(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/StarshipType.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/StarshipType.cs new file mode 100644 index 0000000..7ad6b10 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/StarshipType.cs @@ -0,0 +1,18 @@ +using HotChocolate.Types; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class StarshipType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.GetLength(default, default)); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/SubscriptionType.cs b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/SubscriptionType.cs new file mode 100644 index 0000000..ed872e5 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/StarWars/Types/SubscriptionType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace StarWars.Types +{ + public class SubscriptionType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.OnReview(default, default)) + .Type>() + .Argument("episode", arg => arg.Type>()); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/3_operations/global.json b/kiev_dotnetfest_2019/Demos/3_operations/global.json new file mode 100644 index 0000000..79422f0 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/3_operations/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "3.0.100" + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/DataLoader.csproj b/kiev_dotnetfest_2019/Demos/4_dataloader/DataLoader.csproj new file mode 100644 index 0000000..1dd0b2f --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/DataLoader.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp2.2 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + + + diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/Message.cs b/kiev_dotnetfest_2019/Demos/4_dataloader/Message.cs new file mode 100644 index 0000000..5a16590 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/Message.cs @@ -0,0 +1,16 @@ +using System; +using MongoDB.Bson; +using HotChocolate.Language; + +namespace HotChocolate.Examples.Paging +{ + public class Message + { + public ObjectId Id { get; set; } + public string Text { get; set; } + public DateTimeOffset Created { get; set; } + public int Favorites { get; set; } + public ObjectId UserId { get; set; } + public ObjectId? ReplyToId { get; set; } + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/MessageInput.cs b/kiev_dotnetfest_2019/Demos/4_dataloader/MessageInput.cs new file mode 100644 index 0000000..22b7823 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/MessageInput.cs @@ -0,0 +1,11 @@ +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class MessageInput + { + public string Text { get; set; } + public ObjectId UserId { get; set; } + public ObjectId? ReplyToId { get; set; } + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/MessageInputType.cs b/kiev_dotnetfest_2019/Demos/4_dataloader/MessageInputType.cs new file mode 100644 index 0000000..249be7d --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/MessageInputType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class MessageInputType + : InputObjectType + { + protected override void Configure(IInputObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Text).Type>(); + descriptor.Field(t => t.UserId).Type>(); + descriptor.Field(t => t.ReplyToId).Type(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/MessageRepository.cs b/kiev_dotnetfest_2019/Demos/4_dataloader/MessageRepository.cs new file mode 100644 index 0000000..ad224ae --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/MessageRepository.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace HotChocolate.Examples.Paging +{ + public class MessageRepository + { + private readonly IMongoCollection _messageCollection; + + public MessageRepository(IMongoCollection messageCollection) + { + _messageCollection = messageCollection + ?? throw new ArgumentNullException(nameof(messageCollection)); + } + + public IQueryable GetAllMessages() + { + return _messageCollection.AsQueryable(); + } + + public Task GetMessageById(ObjectId messageId) + { + return _messageCollection.AsQueryable().FirstOrDefaultAsync(t => t.Id == messageId); + } + + public Task CreateMessageAsync(Message message, CancellationToken cancellationToken) + { + return _messageCollection.InsertOneAsync(message, new InsertOneOptions(), cancellationToken); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/MessageType.cs b/kiev_dotnetfest_2019/Demos/4_dataloader/MessageType.cs new file mode 100644 index 0000000..4b9fb1d --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/MessageType.cs @@ -0,0 +1,44 @@ +using HotChocolate.Resolvers; +using HotChocolate.Types; +using GreenDonut; +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class MessageType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Text).Type>(); + descriptor.Field("createdBy").Type>().Resolver(ctx => + { + UserRepository repository = ctx.Service(); + + IDataLoader dataLoader = ctx.BatchDataLoader( + "UserById", + repository.GetUsersAsync); + + return dataLoader.LoadAsync(ctx.Parent().UserId); + }); + descriptor.Field("replyTo").Type().Resolver(async ctx => + { + ObjectId? replyToId = ctx.Parent().ReplyToId; + if (replyToId.HasValue) + { + MessageRepository repository = ctx.Service(); + + IDataLoader dataLoader = ctx.CacheDataLoader( + "MessageById", + repository.GetMessageById); + + return await dataLoader.LoadAsync(ctx.Parent().ReplyToId.Value); + } + return null; + }); + descriptor.Ignore(t => t.UserId); + descriptor.Ignore(t => t.ReplyToId); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/Mutation.cs b/kiev_dotnetfest_2019/Demos/4_dataloader/Mutation.cs new file mode 100644 index 0000000..2aae183 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/Mutation.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace HotChocolate.Examples.Paging +{ + public class Mutation + { + public async Task CreateMessageAsync( + MessageInput messageInput, + [Service]MessageRepository repository, + CancellationToken cancellationToken) + { + var message = new Message + { + Text = messageInput.Text, + UserId = messageInput.UserId, + ReplyToId = messageInput.ReplyToId, + Created = DateTimeOffset.UtcNow, + }; + + await repository.CreateMessageAsync(message, cancellationToken); + + return message; + } + + public async Task CreateUserAsync( + UserInput userInput, + [Service]UserRepository repository, + CancellationToken cancellationToken) + { + var user = new User + { + Name = userInput.Name, + Country = userInput.Country + }; + + await repository.CreateUserAsync(user, cancellationToken); + + return user; + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/MutationType.cs b/kiev_dotnetfest_2019/Demos/4_dataloader/MutationType.cs new file mode 100644 index 0000000..18bfcef --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/MutationType.cs @@ -0,0 +1,19 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class MutationType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.CreateMessageAsync(default, default, default)) + .Argument("messageInput", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.CreateUserAsync(default, default, default)) + .Argument("userInput", a => a.Type>()) + .Type(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/Program.cs b/kiev_dotnetfest_2019/Demos/4_dataloader/Program.cs new file mode 100644 index 0000000..3c1cee5 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace HotChocolate.Examples.Paging +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/Query.cs b/kiev_dotnetfest_2019/Demos/4_dataloader/Query.cs new file mode 100644 index 0000000..c3d5ae6 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/Query.cs @@ -0,0 +1,12 @@ +using System.Linq; + +namespace HotChocolate.Examples.Paging +{ + public class Query + { + public IQueryable GetMessages([Service]MessageRepository repository) + { + return repository.GetAllMessages(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/QueryType.cs b/kiev_dotnetfest_2019/Demos/4_dataloader/QueryType.cs new file mode 100644 index 0000000..b44ce08 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/QueryType.cs @@ -0,0 +1,34 @@ +using GreenDonut; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using HotChocolate.Types.Relay; + +namespace HotChocolate.Examples.Paging +{ + public class QueryType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetMessages(default)) + .UsePaging() + .UseFiltering() + .UseSorting(); + + descriptor.Field("usersByCountry") + .Argument("country", a => a.Type>()) + .Type>>>() + .Resolver(ctx => + { + var userRepository = ctx.Service(); + + IDataLoader userDataLoader = + ctx.GroupDataLoader( + "usersByCountry", + userRepository.GetUsersByCountry); + + return userDataLoader.LoadAsync(ctx.Argument("country")); + }); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/README.md b/kiev_dotnetfest_2019/Demos/4_dataloader/README.md new file mode 100644 index 0000000..760b464 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/README.md @@ -0,0 +1,21 @@ +# DataLoader Example + +This example shows how DataLoader can be used with Hot Chocolate and uses _Mongo_ as database. + +The **Cache DataLoader** and the **Batch DataLoader** are used in `MessageType.cs`. + +The **GroupDataLoader** is used in `QueryType.cs`. + +We support more DataLoader scenarious with Hot Chocolate than are showcased with this example. The example is aimed to show the most common use-cases. + +## Setup Mongo + +Personally I used docker to host my mongo db for the example. If you have setup docker that just add the following line in your terminal emulator of choice: + +```bash +docker run --name mongo -p 27017:27017 -d mongo mongod +``` + +If you don't have docker or do not want to use it you can install mongo from here: [https://www.mongodb.com/download-center/community](https://www.mongodb.com/download-center/community). + +[Hot Chocolate Documentation](https://hotchocolate.io) diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/Startup.cs b/kiev_dotnetfest_2019/Demos/4_dataloader/Startup.cs new file mode 100644 index 0000000..d445da4 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/Startup.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.AspNetCore; +using HotChocolate; +using HotChocolate.Execution.Configuration; +using MongoDB.Driver; +using HotChocolate.Utilities; +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + // setup type conversion for object id + TypeConversion.Default.Register(from => ObjectId.Parse(from)); + TypeConversion.Default.Register(from => from.ToString()); + + // setup the repositories + services.AddSingleton(new MongoClient("mongodb://127.0.0.1:27017")); + services.AddSingleton(s => s.GetRequiredService().GetDatabase("PagingDemo")); + services.AddSingleton>(s => s.GetRequiredService().GetCollection("messages")); + services.AddSingleton>(s => s.GetRequiredService().GetCollection("users")); + services.AddSingleton(); + services.AddSingleton(); + + // this enables you to use DataLoader in your resolvers. + services.AddDataLoaderRegistry(); + + // Add GraphQL Services + services.AddGraphQL(SchemaBuilder.New() + .AddQueryType() + .AddMutationType()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(); + app.UsePlayground(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/User.cs b/kiev_dotnetfest_2019/Demos/4_dataloader/User.cs new file mode 100644 index 0000000..155c78b --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/User.cs @@ -0,0 +1,11 @@ +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class User + { + public ObjectId Id { get; set; } + public string Name { get; set; } + public string Country { get; set; } + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/UserInput.cs b/kiev_dotnetfest_2019/Demos/4_dataloader/UserInput.cs new file mode 100644 index 0000000..2511647 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/UserInput.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.Examples.Paging +{ + public class UserInput + { + public string Name { get; set; } + public string Country { get; set; } + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/UserInputType.cs b/kiev_dotnetfest_2019/Demos/4_dataloader/UserInputType.cs new file mode 100644 index 0000000..1e38cc0 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/UserInputType.cs @@ -0,0 +1,14 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class UserInputType + : InputObjectType + { + protected override void Configure(IInputObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Name).Type>(); + descriptor.Field(t => t.Country).Type>(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/UserRepository.cs b/kiev_dotnetfest_2019/Demos/4_dataloader/UserRepository.cs new file mode 100644 index 0000000..90399ed --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/UserRepository.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace HotChocolate.Examples.Paging +{ + public class UserRepository + { + private readonly IMongoCollection _userCollection; + + public UserRepository(IMongoCollection userCollection) + { + _userCollection = userCollection + ?? throw new ArgumentNullException(nameof(userCollection)); + } + + public IQueryable GetAllUsers() + { + return _userCollection.AsQueryable(); + } + + public async Task> GetUsersByCountry( + IReadOnlyList countries, + CancellationToken cancellationToken) + { + var filters = new List>(); + + foreach (string country in countries) + { + filters.Add(Builders.Filter.Eq(u => u.Country, country)); + } + + List users = await _userCollection + .Find(Builders.Filter.Or(filters)) + .ToListAsync(cancellationToken); + + return users.ToLookup(t => t.Country); + } + + public Task GetUserAsync(ObjectId userId, CancellationToken cancellationToken) + { + return _userCollection.Find(c => c.Id == userId) + .FirstOrDefaultAsync(cancellationToken); + } + + public Task CreateUserAsync(User user, CancellationToken cancellationToken) + { + return _userCollection.InsertOneAsync(user, new InsertOneOptions(), cancellationToken); + } + + public async Task> GetUsersAsync( + IReadOnlyCollection userIds, + CancellationToken cancellationToken) + { + var filters = new List>(); + foreach (ObjectId userId in userIds) + { + filters.Add(Builders.Filter.Eq(u => u.Id, userId)); + } + + List users = await _userCollection + .Find(Builders.Filter.Or(filters)) + .ToListAsync(cancellationToken); + + return users.ToDictionary(t => t.Id); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/UserType.cs b/kiev_dotnetfest_2019/Demos/4_dataloader/UserType.cs new file mode 100644 index 0000000..7af7a05 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/UserType.cs @@ -0,0 +1,14 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class UserType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Name).Type>(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/copy.txt b/kiev_dotnetfest_2019/Demos/4_dataloader/copy.txt new file mode 100644 index 0000000..4248412 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/copy.txt @@ -0,0 +1,23 @@ +MessageType + +descriptor.Field("createdBy").Type>().Resolver(ctx => +{ + UserRepository repository = ctx.Service(); + + IDataLoader dataLoader = ctx.BatchDataLoader( + "UserById", + repository.GetUsersAsync); + + return dataLoader.LoadAsync(ctx.Parent().UserId); +}); + + +IDataLoader dataLoader = ctx.CacheDataLoader( + "MessageById", + repository.GetMessageById); + +return await dataLoader.LoadAsync(ctx.Parent().ReplyToId.Value); + +.UsePaging() + +docker run --name mongo -p 27017:27017 -d mongo mongod \ No newline at end of file diff --git a/kiev_dotnetfest_2019/Demos/4_dataloader/global.json b/kiev_dotnetfest_2019/Demos/4_dataloader/global.json new file mode 100644 index 0000000..89b3b0f --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/4_dataloader/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "2.2.402" + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/.vscode/launch.json b/kiev_dotnetfest_2019/Demos/5_stitching/.vscode/launch.json new file mode 100644 index 0000000..d883fe4 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/Gateway/bin/Debug/netcoreapp2.1/stitched.dll", + "args": [], + "cwd": "${workspaceFolder}/Gateway", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/.vscode/tasks.json b/kiev_dotnetfest_2019/Demos/5_stitching/.vscode/tasks.json new file mode 100644 index 0000000..31c32bd --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/ContractStorage.cs b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/ContractStorage.cs new file mode 100644 index 0000000..57295c7 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/ContractStorage.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Demo.Contracts +{ + public class ContractStorage + { + public List Contracts { get; } = new List + { + new LifeInsuranceContract + { + Id = "1", + CustomerId= "1", + Premium = 123456.11 + }, + new LifeInsuranceContract + { + Id = "2", + CustomerId= "1", + Premium = 456789.12 + }, + new LifeInsuranceContract + { + Id = "3", + CustomerId = "2", + Premium = 789.12 + }, + new SomeOtherContract + { + Id = "1", + CustomerId= "1", + ExpiryDate = new DateTime(2015, 2, 1, 0,0,0, DateTimeKind.Utc) + }, + new SomeOtherContract + { + Id = "2", + CustomerId= "2", + ExpiryDate = new DateTime(2015, 5, 1, 0,0,0, DateTimeKind.Utc) + }, + new SomeOtherContract + { + Id = "3", + CustomerId= "3", + ExpiryDate = new DateTime(2017, 1, 30, 0,0,0, DateTimeKind.Utc) + }, + new SomeOtherContract + { + Id = "4", + CustomerId= "3", + ExpiryDate = new DateTime(2020, 1, 1, 0,0,0, DateTimeKind.Utc) + } + }; + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/ContractType.cs b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/ContractType.cs new file mode 100644 index 0000000..ff5b38b --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/ContractType.cs @@ -0,0 +1,14 @@ +using HotChocolate.Types; + +namespace Demo.Contracts +{ + public class ContractType + : InterfaceType + { + protected override void Configure(IInterfaceTypeDescriptor descriptor) + { + descriptor.Name("Contract"); + descriptor.Field("id").Type>(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/IContract.cs b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/IContract.cs new file mode 100644 index 0000000..64397c4 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/IContract.cs @@ -0,0 +1,9 @@ +namespace Demo.Contracts +{ + public interface IContract + { + string Id { get; } + + string CustomerId { get; } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/LifeInsuranceContract.cs b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/LifeInsuranceContract.cs new file mode 100644 index 0000000..93a577a --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/LifeInsuranceContract.cs @@ -0,0 +1,12 @@ +namespace Demo.Contracts +{ + public class LifeInsuranceContract + : IContract + { + public string Id { get; set; } + + public string CustomerId { get; set; } + + public double Premium { get; set; } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/LifeInsuranceContractType.cs b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/LifeInsuranceContractType.cs new file mode 100644 index 0000000..c629ecf --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/LifeInsuranceContractType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Types; + +namespace Demo.Contracts +{ + public class LifeInsuranceContractType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.CustomerId).Ignore(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/Program.cs b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/Program.cs new file mode 100644 index 0000000..4ec4019 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Demo.Contracts +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseUrls("http://localhost:5051") + .UseStartup(); + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/Query.cs b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/Query.cs new file mode 100644 index 0000000..363b8f7 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/Query.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HotChocolate.Types.Relay; + +namespace Demo.Contracts +{ + public class Query + { + private readonly IdSerializer _idSerializer = new IdSerializer(); + private readonly ContractStorage _contractStorage; + + public Query(ContractStorage contractStorage) + { + _contractStorage = contractStorage + ?? throw new ArgumentNullException(nameof(contractStorage)); + } + + public IContract GetContract(string contractId) + { + IdValue value = _idSerializer.Deserialize(contractId); + + if (value.TypeName == nameof(LifeInsuranceContract)) + { + return _contractStorage.Contracts + .OfType() + .FirstOrDefault(t => t.Id.Equals(value.Value)); + } + else + { + return _contractStorage.Contracts + .OfType() + .FirstOrDefault(t => t.Id.Equals(value.Value)); + } + } + + public IEnumerable GetContracts(string customerId) + { + IdValue value = _idSerializer.Deserialize(customerId); + + return _contractStorage.Contracts + .Where(t => t.CustomerId.Equals(value.Value)); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/QueryType.cs b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/QueryType.cs new file mode 100644 index 0000000..c012283 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/QueryType.cs @@ -0,0 +1,20 @@ +using HotChocolate.Types; + +namespace Demo.Contracts +{ + public class QueryType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetContract(default)) + .Argument("contractId", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.GetContracts(default)) + .Argument("customerId", a => a.Type>()) + .Type>>(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/SomeOtherContract.cs b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/SomeOtherContract.cs new file mode 100644 index 0000000..54c1572 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/SomeOtherContract.cs @@ -0,0 +1,14 @@ +using System; + +namespace Demo.Contracts +{ + public class SomeOtherContract + : IContract + { + public string Id { get; set; } + + public string CustomerId { get; set; } + + public DateTime ExpiryDate { get; set; } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/SomeOtherContractType.cs b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/SomeOtherContractType.cs new file mode 100644 index 0000000..303d676 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/SomeOtherContractType.cs @@ -0,0 +1,23 @@ +using HotChocolate.Types; + +namespace Demo.Contracts +{ + public class SomeOtherContractType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.CustomerId) + .Ignore(); + + descriptor.Field(t => t.ExpiryDate) + .Type>(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/Startup.cs b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/Startup.cs new file mode 100644 index 0000000..71ea528 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/Startup.cs @@ -0,0 +1,39 @@ +using HotChocolate; +using HotChocolate.AspNetCore; +using Demo.Contracts; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Demo.Contracts +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + + // Add GraphQL Services + services.AddGraphQL(Schema.Create(c => + { + c.RegisterQueryType(); + c.RegisterType(); + c.RegisterType(); + + c.UseGlobalObjectIdentifier(); + })); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(); + app.UsePlayground(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/contract.csproj b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/contract.csproj new file mode 100644 index 0000000..c36ea18 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/ContractSchema/contract.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp2.1 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + + + \ No newline at end of file diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/Consultant.cs b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/Consultant.cs new file mode 100644 index 0000000..903e6c5 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/Consultant.cs @@ -0,0 +1,9 @@ +namespace Demo.Customers +{ + public class Consultant + : ICustomerOrConsultant + { + public string Id { get; set; } + public string Name { get; set; } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/ConsultantType.cs b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/ConsultantType.cs new file mode 100644 index 0000000..d3d3e94 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/ConsultantType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace Demo.Customers +{ + public class ConsultantType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Name).Type>(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/Customer.cs b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/Customer.cs new file mode 100644 index 0000000..a0c5abc --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/Customer.cs @@ -0,0 +1,10 @@ +namespace Demo.Customers +{ + public class Customer + : ICustomerOrConsultant + { + public string Id { get; set; } + public string Name { get; set; } + public string ConsultantId { get; set; } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/CustomerOrConsultantType.cs b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/CustomerOrConsultantType.cs new file mode 100644 index 0000000..e6aed53 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/CustomerOrConsultantType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace Demo.Customers +{ + public class CustomerOrConsultantType + : UnionType + { + protected override void Configure(IUnionTypeDescriptor descriptor) + { + descriptor.Name("CustomerOrConsultant"); + descriptor.Type(); + descriptor.Type(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/CustomerRepository.cs b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/CustomerRepository.cs new file mode 100644 index 0000000..0b11ec7 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/CustomerRepository.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace Demo.Customers +{ + public class CustomerRepository + { + public List Customers { get; } = new List + { + new Customer + { + Id = "1", + Name = "Freddy Freeman", + ConsultantId = "1" + }, + new Customer + { + Id = "2", + Name = "Carol Danvers", + ConsultantId = "1" + }, + new Customer + { + Id = "3", + Name = "Walter Lawson", + ConsultantId = "2" + } + }; + + public List Consultants { get; } = new List + { + new Consultant + { + Id = "1", + Name = "Jordan Belfort", + }, + new Consultant + { + Id = "2", + Name = "Gordon Gekko", + } + }; + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/CustomerResolver.cs b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/CustomerResolver.cs new file mode 100644 index 0000000..f30784e --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/CustomerResolver.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq; +using HotChocolate; + +namespace Demo.Customers +{ + public class CustomerResolver + { + public Consultant GetConsultant( + Customer customer, + [Service]CustomerRepository repository) + { + if (customer.ConsultantId != null) + { + return repository.Consultants.FirstOrDefault( + t => t.Id.Equals(customer.ConsultantId)); + } + return null; + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/CustomerType.cs b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/CustomerType.cs new file mode 100644 index 0000000..79e2d17 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/CustomerType.cs @@ -0,0 +1,20 @@ +using HotChocolate.Types; + +namespace Demo.Customers +{ + public class CustomerType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Name).Type>(); + descriptor.Field(t => t.ConsultantId).Ignore(); + + descriptor.Field( + t => t.GetConsultant(default, default)) + .Type(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/ICustomerOrConsultant.cs b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/ICustomerOrConsultant.cs new file mode 100644 index 0000000..17abcb0 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/ICustomerOrConsultant.cs @@ -0,0 +1,7 @@ +namespace Demo.Customers +{ + public interface ICustomerOrConsultant + { + + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/Program.cs b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/Program.cs new file mode 100644 index 0000000..15df5b0 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Demo.Customers +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseUrls("http://localhost:5050") + .UseStartup(); + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/Query.cs b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/Query.cs new file mode 100644 index 0000000..f4a25e9 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/Query.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HotChocolate.Types.Relay; + +namespace Demo.Customers +{ + public class Query + { + private readonly IdSerializer _idSerializer = new IdSerializer(); + private readonly CustomerRepository _repository; + + public Query(CustomerRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + public Customer GetCustomer(string id) + { + IdValue value = _idSerializer.Deserialize(id); + return _repository.Customers + .FirstOrDefault(t => t.Id.Equals(value.Value)); + } + + public IEnumerable GetCustomers() + { + return _repository.Customers; + } + + public Consultant GetConsultant(string id) + { + IdValue value = _idSerializer.Deserialize(id); + return _repository.Consultants + .FirstOrDefault(t => t.Id.Equals(value.Value)); + } + + public ICustomerOrConsultant GetCustomerOrConsultant(string id) + { + IdValue value = _idSerializer.Deserialize(id); + if (value.TypeName == "Consultant") + { + return GetConsultant(id); + } + return GetCustomer(id); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/QueryType.cs b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/QueryType.cs new file mode 100644 index 0000000..183bae0 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/QueryType.cs @@ -0,0 +1,27 @@ +using HotChocolate.Types; + +namespace Demo.Customers +{ + public class QueryType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetCustomer(default)) + .Argument("id", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.GetCustomers()) + .Type>>>(); + + descriptor.Field(t => t.GetConsultant(default)) + .Argument("id", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.GetCustomerOrConsultant(default)) + .Argument("id", a => a.Type>()) + .Type(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/Startup.cs b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/Startup.cs new file mode 100644 index 0000000..4bcace5 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/Startup.cs @@ -0,0 +1,35 @@ +using HotChocolate; +using HotChocolate.AspNetCore; +using Demo.Customers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Demo.Customers +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + + // Add GraphQL Services + services.AddGraphQL(Schema.Create(c => + { + c.RegisterQueryType(); + c.UseGlobalObjectIdentifier(); + })); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(); + app.UsePlayground(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/customer.csproj b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/customer.csproj new file mode 100644 index 0000000..c36ea18 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/CustomerSchema/customer.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp2.1 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + + + \ No newline at end of file diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/.vscode/launch.json b/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/.vscode/launch.json new file mode 100644 index 0000000..2ed183d --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.1/stitched.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart", + "launchBrowser": { + "enabled": true, + "args": "${auto-detect-url}", + "windows": { + "command": "cmd.exe", + "args": "/C start ${auto-detect-url}" + }, + "osx": { + "command": "open" + }, + "linux": { + "command": "xdg-open" + } + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/.vscode/tasks.json b/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/.vscode/tasks.json new file mode 100644 index 0000000..a914fef --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet build", + "type": "shell", + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/Extensions.graphql b/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/Extensions.graphql new file mode 100644 index 0000000..97b9272 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/Extensions.graphql @@ -0,0 +1,12 @@ +type Query { + me: Customer + @delegate( + schema: "customer" + path: "customer(id:$contextData:currentUserId)" + ) +} + +extend type Customer { + contracts: [Contract!] + @delegate(schema: "contract", path: "contracts(customerId:$fields:id)") +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/Program.cs b/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/Program.cs new file mode 100644 index 0000000..f629620 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/Program.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace Demo.Stitching +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/SomeOtherContractExtension.cs b/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/SomeOtherContractExtension.cs new file mode 100644 index 0000000..2fe6fa7 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/SomeOtherContractExtension.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using HotChocolate; +using HotChocolate.Stitching; +using HotChocolate.Types; + +namespace Demo.Stitching +{ + public class SomeOtherContractExtension + : ObjectTypeExtension + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name("SomeOtherContract"); + descriptor.Field("expiresInDays") + .Type>() + .Directive(new ComputedDirective { DependantOn = new NameString[] { "expiryDate" } }) + .Resolver(context => + { + var obj = context.Parent>(); + var serializedExpiryDate = obj["expiryDate"]; + var dateType = (ISerializableType)context.ObjectType.Fields["expiryDate"].Type; + var offset = (DateTimeOffset)dateType.Deserialize(serializedExpiryDate); + return offset.DateTime.Subtract(DateTime.UtcNow).Days; + }); + } + } + +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/Startup.cs b/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/Startup.cs new file mode 100644 index 0000000..8a6d605 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/Startup.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.AspNetCore; +using HotChocolate.Stitching; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Demo.Stitching +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + // Setup the clients that shall be used to access the remote endpoints. + services.AddHttpClient("customer", (sp, client) => + { + // in order to pass on the token or any other headers to the backend schema use the IHttpContextAccessor + HttpContext context = sp.GetRequiredService().HttpContext; + client.BaseAddress = new Uri("http://127.0.0.1:5050"); + }); + + services.AddHttpClient("contract", (sp, client) => + { + // in order to pass on the token or any other headers to the backend schema use the IHttpContextAccessor + HttpContext context = sp.GetRequiredService().HttpContext; + client.BaseAddress = new Uri("http://127.0.0.1:5051"); + }); + + services.AddHttpContextAccessor(); + + services.AddQueryRequestInterceptor((context, builder, cancellationToken) => + { + builder.AddProperty("currentUserId", "Q3VzdG9tZXIKZDE="); + return Task.CompletedTask; + }); + + services.AddStitchedSchema(builder => builder + .AddSchemaFromHttp("customer") + .AddSchemaFromHttp("contract") + .AddExtensionsFromFile("./Extensions.graphql") + .IgnoreRootTypes() + .RenameType("LifeInsuranceContract", "LifeInsurance") + .AddSchemaConfiguration(c => + { + c.RegisterType(); + })); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(new QueryMiddlewareOptions { EnableSubscriptions = false }); + app.UsePlayground(); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/stitched.csproj b/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/stitched.csproj new file mode 100644 index 0000000..b8aef24 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/Gateway/stitched.csproj @@ -0,0 +1,35 @@ + + + + netcoreapp2.1 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + + + + + + + Always + + + + diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/README.md b/kiev_dotnetfest_2019/Demos/5_stitching/README.md new file mode 100644 index 0000000..d807a0c --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/README.md @@ -0,0 +1,20 @@ +# Schema Stitching Example + +This example shows how you can implement as stitched schema with Hot Chocolate. + +This example consists of the following projects: + +- CustomerSchema + The customer schema contains a GraphQL server that serves up a schema around a customer entity. + +- ContractSchema + The contract schema contains a GraphQL server that serves up a schema that provides insurance contract entities that can be assoicated with customers. + +- Stitching + The stitching project contains a GraphQL server that stitches the former mentiond GraphQL schemas together. + +1. Start the customer and contract servers with `dotnet run` +2. When the former servers are running start the stitching server with `dotnet run` +3. Head over to `http://127.0.0.1/playground` and test out some queries. + +[Hot Chocolate Documentation](https://hotchocolate.io) diff --git a/kiev_dotnetfest_2019/Demos/5_stitching/Stitching.sln b/kiev_dotnetfest_2019/Demos/5_stitching/Stitching.sln new file mode 100644 index 0000000..ca605b6 --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/5_stitching/Stitching.sln @@ -0,0 +1,62 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "contract", "ContractSchema\contract.csproj", "{09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "customer", "CustomerSchema\customer.csproj", "{C538BC50-D306-4AE3-8FE4-38481C27427D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "stitched", "Gateway\stitched.csproj", "{E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|x64.ActiveCfg = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|x64.Build.0 = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|x86.ActiveCfg = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|x86.Build.0 = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|Any CPU.Build.0 = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|x64.ActiveCfg = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|x64.Build.0 = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|x86.ActiveCfg = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|x86.Build.0 = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|x64.ActiveCfg = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|x64.Build.0 = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|x86.Build.0 = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|Any CPU.Build.0 = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|x64.ActiveCfg = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|x64.Build.0 = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|x86.ActiveCfg = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|x86.Build.0 = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|x64.ActiveCfg = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|x64.Build.0 = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|x86.ActiveCfg = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|x86.Build.0 = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|Any CPU.Build.0 = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|x64.ActiveCfg = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|x64.Build.0 = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|x86.ActiveCfg = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/kiev_dotnetfest_2019/Demos/6_testing/.vscode/launch.json b/kiev_dotnetfest_2019/Demos/6_testing/.vscode/launch.json new file mode 100644 index 0000000..cc09e8b --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/6_testing/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/netcoreapp3.0/Testing.dll", + "args": [], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/kiev_dotnetfest_2019/Demos/6_testing/.vscode/tasks.json b/kiev_dotnetfest_2019/Demos/6_testing/.vscode/tasks.json new file mode 100644 index 0000000..31c32bd --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/6_testing/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/kiev_dotnetfest_2019/Demos/6_testing/Testing.csproj b/kiev_dotnetfest_2019/Demos/6_testing/Testing.csproj new file mode 100644 index 0000000..43d1d5f --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/6_testing/Testing.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.0 + _9_testing + + false + + + + + + + + + + + + diff --git a/kiev_dotnetfest_2019/Demos/6_testing/UnitTest1.cs b/kiev_dotnetfest_2019/Demos/6_testing/UnitTest1.cs new file mode 100644 index 0000000..094658a --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/6_testing/UnitTest1.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Execution; +using HotChocolate.Types; +using Snapshooter.Xunit; +using Xunit; + +namespace Testing +{ + public class MyTests + { + [Fact] + public void Schema_Snapshot() + { + // arrange + // act + ISchema schema = SchemaBuilder.New() + .AddQueryType(d => d + .Name("Query") + .Field("foo") + .Resolver("bar")) + .Create(); + + // assert + schema.ToString().MatchSnapshot(); + } + + [Fact] + public async Task Schema_Integration_Test() + { + // arrange + ISchema schema = SchemaBuilder.New() + .AddQueryType(d => + { + d.Name("Query"); + d.Field("foo").Resolver("bar"); + d.Field("baz").Resolver(DateTimeOffset.UtcNow); + }) + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + // act + IExecutionResult result = await executor.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ foo baz }") + .Create()); + + // assert + result.MatchSnapshot(matchOptions => matchOptions.IgnoreField("Data.baz")); + } + } +} diff --git a/kiev_dotnetfest_2019/Demos/nuget.config b/kiev_dotnetfest_2019/Demos/nuget.config new file mode 100644 index 0000000..375d64d --- /dev/null +++ b/kiev_dotnetfest_2019/Demos/nuget.config @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/kiev_dotnetfest_2019/GraphQL_Intro.pptx b/kiev_dotnetfest_2019/GraphQL_Intro.pptx new file mode 100644 index 0000000..af2cf87 Binary files /dev/null and b/kiev_dotnetfest_2019/GraphQL_Intro.pptx differ diff --git a/minsk_dotnetsummitby_2019/GraphQL_Stitching_with_Hot_Chocolate.pdf b/minsk_dotnetsummitby_2019/GraphQL_Stitching_with_Hot_Chocolate.pdf new file mode 100644 index 0000000..4c68b87 Binary files /dev/null and b/minsk_dotnetsummitby_2019/GraphQL_Stitching_with_Hot_Chocolate.pdf differ diff --git a/moscow_dotnext_2019/1_graphql_vs_rest/index.html b/moscow_dotnext_2019/1_graphql_vs_rest/index.html new file mode 100644 index 0000000..889c94b --- /dev/null +++ b/moscow_dotnext_2019/1_graphql_vs_rest/index.html @@ -0,0 +1,268 @@ + + + + + GraphQL vs. REST + + + +
    + + vs. + +
    +
    + Loading... +
    +
    +

    + An unsorted list of characters who played in the same films where Luke + Skywalker were present +

    +

    + Used and returned + characters which took + ms. +

    +
      +
      + + + diff --git a/moscow_dotnext_2019/2_hello_world/.vscode/launch.json b/moscow_dotnext_2019/2_hello_world/.vscode/launch.json new file mode 100644 index 0000000..c509473 --- /dev/null +++ b/moscow_dotnext_2019/2_hello_world/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp3.0/2_hello_world.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/moscow_dotnext_2019/2_hello_world/.vscode/tasks.json b/moscow_dotnext_2019/2_hello_world/.vscode/tasks.json new file mode 100644 index 0000000..31c32bd --- /dev/null +++ b/moscow_dotnext_2019/2_hello_world/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/moscow_dotnext_2019/2_hello_world/2_hello_world.csproj b/moscow_dotnext_2019/2_hello_world/2_hello_world.csproj new file mode 100644 index 0000000..b3d059f --- /dev/null +++ b/moscow_dotnext_2019/2_hello_world/2_hello_world.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.0 + _2_hello_world + + + + + + + diff --git a/moscow_dotnext_2019/2_hello_world/Program.cs b/moscow_dotnext_2019/2_hello_world/Program.cs new file mode 100644 index 0000000..7a86352 --- /dev/null +++ b/moscow_dotnext_2019/2_hello_world/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace HelloWorld +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/moscow_dotnext_2019/2_hello_world/Query.cs b/moscow_dotnext_2019/2_hello_world/Query.cs new file mode 100644 index 0000000..2d3b5ba --- /dev/null +++ b/moscow_dotnext_2019/2_hello_world/Query.cs @@ -0,0 +1,7 @@ +namespace HelloWorld +{ + public class Query + { + public string Hello() => "world"; + } +} diff --git a/moscow_dotnext_2019/2_hello_world/QueryType.cs b/moscow_dotnext_2019/2_hello_world/QueryType.cs new file mode 100644 index 0000000..36c0ca3 --- /dev/null +++ b/moscow_dotnext_2019/2_hello_world/QueryType.cs @@ -0,0 +1,12 @@ +using HotChocolate.Types; + +namespace HelloWorld +{ + public class QueryType : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Hello()).Type>(); + } + } +} diff --git a/moscow_dotnext_2019/2_hello_world/Startup.cs b/moscow_dotnext_2019/2_hello_world/Startup.cs new file mode 100644 index 0000000..82be229 --- /dev/null +++ b/moscow_dotnext_2019/2_hello_world/Startup.cs @@ -0,0 +1,26 @@ +using HotChocolate; +using HotChocolate.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace HelloWorld +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddGraphQL(SchemaBuilder.New() + .AddQueryType() + .Create()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseGraphQL(); + } + } +} diff --git a/moscow_dotnext_2019/2_hello_world/appsettings.Development.json b/moscow_dotnext_2019/2_hello_world/appsettings.Development.json new file mode 100644 index 0000000..a2880cb --- /dev/null +++ b/moscow_dotnext_2019/2_hello_world/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/moscow_dotnext_2019/2_hello_world/appsettings.json b/moscow_dotnext_2019/2_hello_world/appsettings.json new file mode 100644 index 0000000..81ff877 --- /dev/null +++ b/moscow_dotnext_2019/2_hello_world/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/moscow_dotnext_2019/2_hello_world/global.json b/moscow_dotnext_2019/2_hello_world/global.json new file mode 100644 index 0000000..79422f0 --- /dev/null +++ b/moscow_dotnext_2019/2_hello_world/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "3.0.100" + } +} diff --git a/moscow_dotnext_2019/3_operations/.vscode/launch.json b/moscow_dotnext_2019/3_operations/.vscode/launch.json new file mode 100644 index 0000000..d3f889e --- /dev/null +++ b/moscow_dotnext_2019/3_operations/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/StarWars/bin/Debug/netcoreapp3.0/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/moscow_dotnext_2019/3_operations/.vscode/tasks.json b/moscow_dotnext_2019/3_operations/.vscode/tasks.json new file mode 100644 index 0000000..383f2db --- /dev/null +++ b/moscow_dotnext_2019/3_operations/.vscode/tasks.json @@ -0,0 +1,25 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + "StarWars", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/moscow_dotnext_2019/3_operations/StarWars/.vscode/launch.json b/moscow_dotnext_2019/3_operations/StarWars/.vscode/launch.json new file mode 100644 index 0000000..401f807 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.1/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/moscow_dotnext_2019/3_operations/StarWars/.vscode/tasks.json b/moscow_dotnext_2019/3_operations/StarWars/.vscode/tasks.json new file mode 100644 index 0000000..68ae5ff --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet build", + "type": "shell", + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/AspNetCore.StarWars.csproj b/moscow_dotnext_2019/3_operations/StarWars/AspNetCore.StarWars.csproj new file mode 100644 index 0000000..1b041de --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/AspNetCore.StarWars.csproj @@ -0,0 +1,34 @@ + + + + netcoreapp3.0 + false + 7.2 + true + $(NoWarn);1591 + + + + portable + true + + + + pdbonly + true + + + + + + + + + + + + + + + + diff --git a/moscow_dotnext_2019/3_operations/StarWars/Data/CharacterRepository.cs b/moscow_dotnext_2019/3_operations/StarWars/Data/CharacterRepository.cs new file mode 100644 index 0000000..dd09134 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Data/CharacterRepository.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StarWars.Models; + +namespace StarWars.Data +{ + public class CharacterRepository + { + private Dictionary _characters; + private Dictionary _starships; + + public CharacterRepository() + { + _characters = CreateCharacters().ToDictionary(t => t.Id); + _starships = CreateStarships().ToDictionary(t => t.Id); + } + + public ICharacter GetHero(Episode episode) + { + if (episode == Episode.Empire) + { + return _characters["1000"]; + } + return _characters["2001"]; + } + + public ICharacter GetCharacter(string id) + { + if (_characters.TryGetValue(id, out ICharacter c)) + { + return c; + } + return null; + } + + public Human GetHuman(string id) + { + if (_characters.TryGetValue(id, out ICharacter c) + && c is Human h) + { + return h; + } + return null; + } + + public Droid GetDroid(string id) + { + if (_characters.TryGetValue(id, out ICharacter c) + && c is Droid d) + { + return d; + } + return null; + } + + public IEnumerable Search(string text) + { +#if ASPNETCLASSIC + IEnumerable filteredCharacters = _characters.Values + .Where(t => t.Name.Contains(text)); +#else + IEnumerable filteredCharacters = _characters.Values + .Where(t => t.Name.Contains(text, + StringComparison.OrdinalIgnoreCase)); +#endif + + foreach (ICharacter character in filteredCharacters) + { + yield return character; + } + +#if ASPNETCLASSIC + IEnumerable filteredStarships = _starships.Values + .Where(t => t.Name.Contains(text)); +#else + IEnumerable filteredStarships = _starships.Values + .Where(t => t.Name.Contains(text, + StringComparison.OrdinalIgnoreCase)); +#endif + + foreach (Starship starship in filteredStarships) + { + yield return starship; + } + } + + private static IEnumerable CreateCharacters() + { + yield return new Human + { + Id = "1000", + Name = "Luke Skywalker", + Friends = new[] { "1002", "1003", "2000", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Tatooine" + }; + + yield return new Human + { + Id = "1001", + Name = "Darth Vader", + Friends = new[] { "1004" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Tatooine" + }; + + yield return new Human + { + Id = "1002", + Name = "Han Solo", + Friends = new[] { "1000", "1003", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi } + }; + + yield return new Human + { + Id = "1003", + Name = "Leia Organa", + Friends = new[] { "1000", "1002", "2000", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Alderaan" + }; + + yield return new Human + { + Id = "1004", + Name = "Wilhuff Tarkin", + Friends = new[] { "1001" }, + AppearsIn = new[] { Episode.NewHope } + }; + + yield return new Droid + { + Id = "2000", + Name = "C-3PO", + Friends = new[] { "1000", "1002", "1003", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + PrimaryFunction = "Protocol" + }; + + yield return new Droid + { + Id = "2001", + Name = "R2-D2", + Friends = new[] { "1000", "1002", "1003" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + PrimaryFunction = "Astromech" + }; + } + + private static IEnumerable CreateStarships() + { + yield return new Starship + { + Id = "3000", + Name = "TIE Advanced x1", + Length = 9.2 + }; + } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Data/ReviewRepository.cs b/moscow_dotnext_2019/3_operations/StarWars/Data/ReviewRepository.cs new file mode 100644 index 0000000..a586306 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Data/ReviewRepository.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using StarWars.Models; + +namespace StarWars.Data +{ + public class ReviewRepository + { + private readonly Dictionary> _data = + new Dictionary>(); + + public void AddReview(Episode episode, Review review) + { + if (!_data.TryGetValue(episode, out List reviews)) + { + reviews = new List(); + _data[episode] = reviews; + } + + reviews.Add(review); + } + + public IEnumerable GetReviews(Episode episode) + { + if (_data.TryGetValue(episode, out List reviews)) + { + return reviews; + } + return Array.Empty(); + } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Models/Droid.cs b/moscow_dotnext_2019/3_operations/StarWars/Models/Droid.cs new file mode 100644 index 0000000..0ee4213 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Models/Droid.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A droid in the Star Wars universe. + /// + public class Droid + : ICharacter + { + /// + public string Id { get; set; } + + /// + public string Name { get; set; } + + /// + public IReadOnlyList Friends { get; set; } + + /// + public IReadOnlyList AppearsIn { get; set; } + + /// + /// The droid's primary function. + /// + public string PrimaryFunction { get; set; } + + /// + public double Height { get; } = 1.72d; + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Models/Episode.cs b/moscow_dotnext_2019/3_operations/StarWars/Models/Episode.cs new file mode 100644 index 0000000..6900cf6 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Models/Episode.cs @@ -0,0 +1,21 @@ +namespace StarWars.Models +{ + /// + /// The Star Wars episodes. + /// + public enum Episode + { + /// + /// Star Wars Episode IV: A New Hope + /// + NewHope, + /// + /// Star Wars Episode V: Empire Strikes Back + /// + Empire, + /// + /// Star Wars Episode VI: Return of the Jedi + /// + Jedi + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Models/Human.cs b/moscow_dotnext_2019/3_operations/StarWars/Models/Human.cs new file mode 100644 index 0000000..caf6021 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Models/Human.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A human character in the Star Wars universe. + /// + public class Human + : ICharacter + { + /// + public string Id { get; set; } + + /// + public string Name { get; set; } + + /// + public IReadOnlyList Friends { get; set; } + + /// + public IReadOnlyList AppearsIn { get; set; } + + /// + /// The planet the character is originally from. + /// + public string HomePlanet { get; set; } + + /// + public double Height { get; } = 1.72d; + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Models/ICharacter.cs b/moscow_dotnext_2019/3_operations/StarWars/Models/ICharacter.cs new file mode 100644 index 0000000..f9186c1 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Models/ICharacter.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A character in the Star Wars universe. + /// + public interface ICharacter + { + /// + /// The unique identifier for the character. + /// + string Id { get; } + + /// + /// The name of the character. + /// + string Name { get; } + + /// + /// The names of the character's friends. + /// + IReadOnlyList Friends { get; } + + /// + /// The episodes the character appears in. + /// + IReadOnlyList AppearsIn { get; } + + /// + /// The height of the character. + /// + double Height { get; } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Models/Review.cs b/moscow_dotnext_2019/3_operations/StarWars/Models/Review.cs new file mode 100644 index 0000000..3f18f16 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Models/Review.cs @@ -0,0 +1,18 @@ +namespace StarWars.Models +{ + /// + /// A review of a particular movie. + /// + public class Review + { + /// + /// The number of stars given for this review. + /// + public int Stars { get; set; } + + /// + /// An explanation for the rating. + /// + public string Commentary { get; set; } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Models/Starship.cs b/moscow_dotnext_2019/3_operations/StarWars/Models/Starship.cs new file mode 100644 index 0000000..5d7c241 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Models/Starship.cs @@ -0,0 +1,23 @@ +namespace StarWars.Models +{ + /// + /// A starship in the Star Wars universe. + /// + public class Starship + { + /// + /// The Id of the starship. + /// + public string Id { get; set; } + + /// + /// The name of the starship. + /// + public string Name { get; set; } + + /// + /// The length of the starship. + /// + public double Length { get; set; } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Models/Unit.cs b/moscow_dotnext_2019/3_operations/StarWars/Models/Unit.cs new file mode 100644 index 0000000..15316aa --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Models/Unit.cs @@ -0,0 +1,11 @@ +namespace StarWars.Models +{ + /// + /// Different units of measurement. + /// + public enum Unit + { + Foot, + Meters + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Mutation.cs b/moscow_dotnext_2019/3_operations/StarWars/Mutation.cs new file mode 100644 index 0000000..45d4adc --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Mutation.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Subscriptions; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Mutation + { + private readonly ReviewRepository _repository; + + public Mutation(ReviewRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Creates a review for a given Star Wars episode. + /// + /// The episode to review. + /// The review. + /// The event sending service. + /// The created review. + public async Task CreateReview( + Episode episode, Review review, + [Service]IEventSender eventSender) + { + _repository.AddReview(episode, review); + await eventSender.SendAsync(new OnReviewMessage(episode, review)); + return review; + } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/OnReviewMessage.cs b/moscow_dotnext_2019/3_operations/StarWars/OnReviewMessage.cs new file mode 100644 index 0000000..95f9cff --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/OnReviewMessage.cs @@ -0,0 +1,23 @@ +using HotChocolate.Language; +using HotChocolate.Subscriptions; +using StarWars.Models; + +namespace StarWars +{ + public class OnReviewMessage + : EventMessage + { + public OnReviewMessage(Episode episode, Review review) + : base(CreateEventDescription(episode), review) + { + } + + private static EventDescription CreateEventDescription(Episode episode) + { + return new EventDescription("onReview", + new ArgumentNode("episode", + new EnumValueNode( + episode.ToString().ToUpperInvariant()))); + } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Program.cs b/moscow_dotnext_2019/3_operations/StarWars/Program.cs new file mode 100644 index 0000000..0358799 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace StarWars +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Query.cs b/moscow_dotnext_2019/3_operations/StarWars/Query.cs new file mode 100644 index 0000000..887534e --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Query.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using HotChocolate.Resolvers; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Query + { + private readonly CharacterRepository _repository; + + public Query(CharacterRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Retrieve a hero by a particular Star Wars episode. + /// + /// The episode to look up by. + /// The character. + public ICharacter GetHero(Episode episode) + { + return _repository.GetHero(episode); + } + + /// + /// Gets a human by Id. + /// + /// The Id of the human to retrieve. + /// The human. + public Human GetHuman(string id) + { + return _repository.GetHuman(id); + } + + /// + /// Get a particular droid by Id. + /// + /// The Id of the droid. + /// The droid. + public Droid GetDroid(string id) + { + return _repository.GetDroid(id); + } + + public IEnumerable GetCharacter(string[] characterIds, IResolverContext context) + { + foreach (string characterId in characterIds) + { + ICharacter character = _repository.GetCharacter(characterId); + if (character == null) + { + context.ReportError( + "Could not resolve a charachter for the " + + $"character-id {characterId}."); + } + else + { + yield return character; + } + } + } + + public IEnumerable Search(string text) + { + return _repository.Search(text); + } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Resolvers/SharedResolvers.cs b/moscow_dotnext_2019/3_operations/StarWars/Resolvers/SharedResolvers.cs new file mode 100644 index 0000000..3911604 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Resolvers/SharedResolvers.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using HotChocolate; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars.Resolvers +{ + public class SharedResolvers + { + public IEnumerable GetCharacter( + [Parent]ICharacter character, + [Service]CharacterRepository repository) + { + foreach (string friendId in character.Friends) + { + ICharacter friend = repository.GetCharacter(friendId); + if (friend != null) + { + yield return friend; + } + } + } + + public double GetHeight(Unit? unit, [Parent]ICharacter character) + => ConvertToUnit(character.Height, unit); + + public double GetLength(Unit? unit, [Parent]Starship starship) + => ConvertToUnit(starship.Length, unit); + + private double ConvertToUnit(double length, Unit? unit) + { + if (unit == Unit.Foot) + { + return length * 3.28084d; + } + return length; + } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Startup.cs b/moscow_dotnext_2019/3_operations/StarWars/Startup.cs new file mode 100644 index 0000000..b55e4d8 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Startup.cs @@ -0,0 +1,66 @@ +using System.Security.Claims; +using HotChocolate; +using HotChocolate.AspNetCore; +using HotChocolate.AspNetCore.Voyager; +using HotChocolate.Execution.Configuration; +using HotChocolate.Subscriptions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using StarWars.Data; +using StarWars.Types; + +namespace StarWars +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + // Add the custom services like repositories etc ... + services.AddSingleton(); + services.AddSingleton(); + + // Add in-memory event provider + services.AddInMemorySubscriptionProvider(); + + // Add GraphQL Services + services.AddGraphQL(sp => SchemaBuilder.New() + .AddServices(sp) + + // Adds the authorize directive and + // enable the authorization middleware. + .AddAuthorizeDirectiveType() + + .AddQueryType() + .AddMutationType() + .AddSubscriptionType() + .AddType() + .AddType() + .AddType() + .Create()); + + + // Add Authorization Policy + services.AddAuthorization(options => + { + options.AddPolicy("HasCountry", policy => + policy.RequireAssertion(context => + context.User.HasClaim(c => + (c.Type == ClaimTypes.Country)))); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app + .UseWebSockets() + .UseGraphQL("/graphql") + .UseGraphiQL("/graphql") + .UsePlayground("/graphql") + .UseVoyager("/graphql"); + } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Subscription.cs b/moscow_dotnext_2019/3_operations/StarWars/Subscription.cs new file mode 100644 index 0000000..06a88b2 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Subscription.cs @@ -0,0 +1,23 @@ +using System; +using HotChocolate.Subscriptions; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Subscription + { + private readonly ReviewRepository _repository; + + public Subscription(ReviewRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + public Review OnReview(Episode episode, IEventMessage message) + { + return (Review)message.Payload; + } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Types/CharacterType.cs b/moscow_dotnext_2019/3_operations/StarWars/Types/CharacterType.cs new file mode 100644 index 0000000..b9c5c98 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Types/CharacterType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; + +namespace StarWars.Types +{ + public class CharacterType + : InterfaceType + { + protected override void Configure(IInterfaceTypeDescriptor descriptor) + { + descriptor.Name("Character"); + + descriptor.Field(f => f.Id) + .Type>(); + + descriptor.Field(f => f.Name) + .Type(); + + descriptor.Field(f => f.Friends) + .UsePaging(); + + descriptor.Field(f => f.AppearsIn) + .Type>(); + + descriptor.Field(f => f.Height) + .Type() + .Argument("unit", a => a.Type>()); + } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Types/DroidType.cs b/moscow_dotnext_2019/3_operations/StarWars/Types/DroidType.cs new file mode 100644 index 0000000..c34fe9a --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Types/DroidType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class DroidType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.AppearsIn) + .Type>(); + + descriptor.Field(r => r.GetCharacter(default, default)) + .UsePaging() + .Name("friends"); + + descriptor.Field(t => t.GetHeight(default, default)) + .Type() + .Argument("unit", a => a.Type>()) + .Name("height"); + } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Types/EpisodeType.cs b/moscow_dotnext_2019/3_operations/StarWars/Types/EpisodeType.cs new file mode 100644 index 0000000..bfb484d --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Types/EpisodeType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class EpisodeType + : EnumType + { + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Types/HumanType.cs b/moscow_dotnext_2019/3_operations/StarWars/Types/HumanType.cs new file mode 100644 index 0000000..3ab1c6a --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Types/HumanType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class HumanType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.AppearsIn) + .Type>(); + + descriptor.Field(r => r.GetCharacter(default, default)) + .UsePaging() + .Name("friends"); + + descriptor.Field(t => t.GetHeight(default, default)) + .Type() + .Argument("unit", a => a.Type>()) + .Name("height"); + } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Types/MutationType.cs b/moscow_dotnext_2019/3_operations/StarWars/Types/MutationType.cs new file mode 100644 index 0000000..71963b7 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Types/MutationType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Types; + +namespace StarWars.Types +{ + public class MutationType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.CreateReview(default, default, default)) + .Type>() + .Argument("episode", a => a.Type>()) + .Argument("review", a => a.Type>()); + } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Types/QueryType.cs b/moscow_dotnext_2019/3_operations/StarWars/Types/QueryType.cs new file mode 100644 index 0000000..193cf89 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Types/QueryType.cs @@ -0,0 +1,22 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class QueryType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetHero(default)) + .Type() + .Argument("episode", a => a.DefaultValue(Episode.NewHope)); + + descriptor.Field(t => t.GetCharacter(default, default)) + .Type>>>(); + + descriptor.Field(t => t.Search(default)) + .Type>(); + } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Types/ReviewInputType.cs b/moscow_dotnext_2019/3_operations/StarWars/Types/ReviewInputType.cs new file mode 100644 index 0000000..300fbe6 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Types/ReviewInputType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class ReviewInputType + : InputObjectType + { + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Types/ReviewType.cs b/moscow_dotnext_2019/3_operations/StarWars/Types/ReviewType.cs new file mode 100644 index 0000000..be94b38 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Types/ReviewType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class ReviewType + : ObjectType + { + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Types/SearchResultType.cs b/moscow_dotnext_2019/3_operations/StarWars/Types/SearchResultType.cs new file mode 100644 index 0000000..7d1bb88 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Types/SearchResultType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class SearchResultType + : UnionType + { + protected override void Configure(IUnionTypeDescriptor descriptor) + { + descriptor.Name("SearchResult"); + descriptor.Type>(); + descriptor.Type(); + descriptor.Type(); + } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Types/StarshipType.cs b/moscow_dotnext_2019/3_operations/StarWars/Types/StarshipType.cs new file mode 100644 index 0000000..7ad6b10 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Types/StarshipType.cs @@ -0,0 +1,18 @@ +using HotChocolate.Types; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class StarshipType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.GetLength(default, default)); + } + } +} diff --git a/moscow_dotnext_2019/3_operations/StarWars/Types/SubscriptionType.cs b/moscow_dotnext_2019/3_operations/StarWars/Types/SubscriptionType.cs new file mode 100644 index 0000000..ed872e5 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/StarWars/Types/SubscriptionType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace StarWars.Types +{ + public class SubscriptionType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.OnReview(default, default)) + .Type>() + .Argument("episode", arg => arg.Type>()); + } + } +} diff --git a/moscow_dotnext_2019/3_operations/global.json b/moscow_dotnext_2019/3_operations/global.json new file mode 100644 index 0000000..79422f0 --- /dev/null +++ b/moscow_dotnext_2019/3_operations/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "3.0.100" + } +} diff --git a/moscow_dotnext_2019/4_dataloader/DataLoader.csproj b/moscow_dotnext_2019/4_dataloader/DataLoader.csproj new file mode 100644 index 0000000..0bff1c1 --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/DataLoader.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp2.2 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + + \ No newline at end of file diff --git a/moscow_dotnext_2019/4_dataloader/DataLoader.sln b/moscow_dotnext_2019/4_dataloader/DataLoader.sln new file mode 100644 index 0000000..f90e842 --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/DataLoader.sln @@ -0,0 +1,17 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataLoader", "DataLoader.csproj", "{8B0F2085-502A-495C-BD9D-9827B8F3F0EE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8B0F2085-502A-495C-BD9D-9827B8F3F0EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B0F2085-502A-495C-BD9D-9827B8F3F0EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B0F2085-502A-495C-BD9D-9827B8F3F0EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B0F2085-502A-495C-BD9D-9827B8F3F0EE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/moscow_dotnext_2019/4_dataloader/Message.cs b/moscow_dotnext_2019/4_dataloader/Message.cs new file mode 100644 index 0000000..5a16590 --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/Message.cs @@ -0,0 +1,16 @@ +using System; +using MongoDB.Bson; +using HotChocolate.Language; + +namespace HotChocolate.Examples.Paging +{ + public class Message + { + public ObjectId Id { get; set; } + public string Text { get; set; } + public DateTimeOffset Created { get; set; } + public int Favorites { get; set; } + public ObjectId UserId { get; set; } + public ObjectId? ReplyToId { get; set; } + } +} diff --git a/moscow_dotnext_2019/4_dataloader/MessageInput.cs b/moscow_dotnext_2019/4_dataloader/MessageInput.cs new file mode 100644 index 0000000..22b7823 --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/MessageInput.cs @@ -0,0 +1,11 @@ +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class MessageInput + { + public string Text { get; set; } + public ObjectId UserId { get; set; } + public ObjectId? ReplyToId { get; set; } + } +} diff --git a/moscow_dotnext_2019/4_dataloader/MessageInputType.cs b/moscow_dotnext_2019/4_dataloader/MessageInputType.cs new file mode 100644 index 0000000..249be7d --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/MessageInputType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class MessageInputType + : InputObjectType + { + protected override void Configure(IInputObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Text).Type>(); + descriptor.Field(t => t.UserId).Type>(); + descriptor.Field(t => t.ReplyToId).Type(); + } + } +} diff --git a/moscow_dotnext_2019/4_dataloader/MessageRepository.cs b/moscow_dotnext_2019/4_dataloader/MessageRepository.cs new file mode 100644 index 0000000..ad224ae --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/MessageRepository.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace HotChocolate.Examples.Paging +{ + public class MessageRepository + { + private readonly IMongoCollection _messageCollection; + + public MessageRepository(IMongoCollection messageCollection) + { + _messageCollection = messageCollection + ?? throw new ArgumentNullException(nameof(messageCollection)); + } + + public IQueryable GetAllMessages() + { + return _messageCollection.AsQueryable(); + } + + public Task GetMessageById(ObjectId messageId) + { + return _messageCollection.AsQueryable().FirstOrDefaultAsync(t => t.Id == messageId); + } + + public Task CreateMessageAsync(Message message, CancellationToken cancellationToken) + { + return _messageCollection.InsertOneAsync(message, new InsertOneOptions(), cancellationToken); + } + } +} diff --git a/moscow_dotnext_2019/4_dataloader/MessageType.cs b/moscow_dotnext_2019/4_dataloader/MessageType.cs new file mode 100644 index 0000000..9af3531 --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/MessageType.cs @@ -0,0 +1,39 @@ +using HotChocolate.Resolvers; +using HotChocolate.Types; +using GreenDonut; +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class MessageType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Text).Type>(); + descriptor.Field("createdBy").Type>().Resolver(ctx => + { + UserRepository repository = ctx.Service(); + + IDataLoader dataLoader = ctx.BatchDataLoader( + "UserById", + repository.GetUsersAsync); + + return dataLoader.LoadAsync(ctx.Parent().UserId); + }); + descriptor.Field("replyTo").Type().Resolver(async ctx => + { + ObjectId? replyToId = ctx.Parent().ReplyToId; + if (replyToId.HasValue) + { + MessageRepository repository = ctx.Service(); + return await repository.GetMessageById(replyToId.Value); + } + return null; + }); + descriptor.Ignore(t => t.UserId); + descriptor.Ignore(t => t.ReplyToId); + } + } +} diff --git a/moscow_dotnext_2019/4_dataloader/Mutation.cs b/moscow_dotnext_2019/4_dataloader/Mutation.cs new file mode 100644 index 0000000..2aae183 --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/Mutation.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace HotChocolate.Examples.Paging +{ + public class Mutation + { + public async Task CreateMessageAsync( + MessageInput messageInput, + [Service]MessageRepository repository, + CancellationToken cancellationToken) + { + var message = new Message + { + Text = messageInput.Text, + UserId = messageInput.UserId, + ReplyToId = messageInput.ReplyToId, + Created = DateTimeOffset.UtcNow, + }; + + await repository.CreateMessageAsync(message, cancellationToken); + + return message; + } + + public async Task CreateUserAsync( + UserInput userInput, + [Service]UserRepository repository, + CancellationToken cancellationToken) + { + var user = new User + { + Name = userInput.Name, + Country = userInput.Country + }; + + await repository.CreateUserAsync(user, cancellationToken); + + return user; + } + } +} diff --git a/moscow_dotnext_2019/4_dataloader/MutationType.cs b/moscow_dotnext_2019/4_dataloader/MutationType.cs new file mode 100644 index 0000000..18bfcef --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/MutationType.cs @@ -0,0 +1,19 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class MutationType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.CreateMessageAsync(default, default, default)) + .Argument("messageInput", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.CreateUserAsync(default, default, default)) + .Argument("userInput", a => a.Type>()) + .Type(); + } + } +} diff --git a/moscow_dotnext_2019/4_dataloader/Program.cs b/moscow_dotnext_2019/4_dataloader/Program.cs new file mode 100644 index 0000000..3c1cee5 --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace HotChocolate.Examples.Paging +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/moscow_dotnext_2019/4_dataloader/QueryType.cs b/moscow_dotnext_2019/4_dataloader/QueryType.cs new file mode 100644 index 0000000..2bcc309 --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/QueryType.cs @@ -0,0 +1,34 @@ +using GreenDonut; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using HotChocolate.Types.Relay; + +namespace HotChocolate.Examples.Paging +{ + public class QueryType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field("messages") + .Resolver(ctx => ctx.Service().GetAllMessages()) + .UsePaging() + .UseFiltering(); + + descriptor.Field("usersByCountry") + .Argument("country", a => a.Type>()) + .Type>>>() + .Resolver(ctx => + { + var userRepository = ctx.Service(); + + IDataLoader userDataLoader = + ctx.GroupDataLoader( + "usersByCountry", + userRepository.GetUsersByCountry); + + return userDataLoader.LoadAsync(ctx.Argument("country")); + }); + } + } +} diff --git a/moscow_dotnext_2019/4_dataloader/README.md b/moscow_dotnext_2019/4_dataloader/README.md new file mode 100644 index 0000000..760b464 --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/README.md @@ -0,0 +1,21 @@ +# DataLoader Example + +This example shows how DataLoader can be used with Hot Chocolate and uses _Mongo_ as database. + +The **Cache DataLoader** and the **Batch DataLoader** are used in `MessageType.cs`. + +The **GroupDataLoader** is used in `QueryType.cs`. + +We support more DataLoader scenarious with Hot Chocolate than are showcased with this example. The example is aimed to show the most common use-cases. + +## Setup Mongo + +Personally I used docker to host my mongo db for the example. If you have setup docker that just add the following line in your terminal emulator of choice: + +```bash +docker run --name mongo -p 27017:27017 -d mongo mongod +``` + +If you don't have docker or do not want to use it you can install mongo from here: [https://www.mongodb.com/download-center/community](https://www.mongodb.com/download-center/community). + +[Hot Chocolate Documentation](https://hotchocolate.io) diff --git a/moscow_dotnext_2019/4_dataloader/Startup.cs b/moscow_dotnext_2019/4_dataloader/Startup.cs new file mode 100644 index 0000000..d445da4 --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/Startup.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.AspNetCore; +using HotChocolate; +using HotChocolate.Execution.Configuration; +using MongoDB.Driver; +using HotChocolate.Utilities; +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + // setup type conversion for object id + TypeConversion.Default.Register(from => ObjectId.Parse(from)); + TypeConversion.Default.Register(from => from.ToString()); + + // setup the repositories + services.AddSingleton(new MongoClient("mongodb://127.0.0.1:27017")); + services.AddSingleton(s => s.GetRequiredService().GetDatabase("PagingDemo")); + services.AddSingleton>(s => s.GetRequiredService().GetCollection("messages")); + services.AddSingleton>(s => s.GetRequiredService().GetCollection("users")); + services.AddSingleton(); + services.AddSingleton(); + + // this enables you to use DataLoader in your resolvers. + services.AddDataLoaderRegistry(); + + // Add GraphQL Services + services.AddGraphQL(SchemaBuilder.New() + .AddQueryType() + .AddMutationType()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(); + app.UsePlayground(); + } + } +} diff --git a/moscow_dotnext_2019/4_dataloader/User.cs b/moscow_dotnext_2019/4_dataloader/User.cs new file mode 100644 index 0000000..155c78b --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/User.cs @@ -0,0 +1,11 @@ +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class User + { + public ObjectId Id { get; set; } + public string Name { get; set; } + public string Country { get; set; } + } +} diff --git a/moscow_dotnext_2019/4_dataloader/UserInput.cs b/moscow_dotnext_2019/4_dataloader/UserInput.cs new file mode 100644 index 0000000..2511647 --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/UserInput.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.Examples.Paging +{ + public class UserInput + { + public string Name { get; set; } + public string Country { get; set; } + } +} diff --git a/moscow_dotnext_2019/4_dataloader/UserInputType.cs b/moscow_dotnext_2019/4_dataloader/UserInputType.cs new file mode 100644 index 0000000..1e38cc0 --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/UserInputType.cs @@ -0,0 +1,14 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class UserInputType + : InputObjectType + { + protected override void Configure(IInputObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Name).Type>(); + descriptor.Field(t => t.Country).Type>(); + } + } +} diff --git a/moscow_dotnext_2019/4_dataloader/UserRepository.cs b/moscow_dotnext_2019/4_dataloader/UserRepository.cs new file mode 100644 index 0000000..90399ed --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/UserRepository.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace HotChocolate.Examples.Paging +{ + public class UserRepository + { + private readonly IMongoCollection _userCollection; + + public UserRepository(IMongoCollection userCollection) + { + _userCollection = userCollection + ?? throw new ArgumentNullException(nameof(userCollection)); + } + + public IQueryable GetAllUsers() + { + return _userCollection.AsQueryable(); + } + + public async Task> GetUsersByCountry( + IReadOnlyList countries, + CancellationToken cancellationToken) + { + var filters = new List>(); + + foreach (string country in countries) + { + filters.Add(Builders.Filter.Eq(u => u.Country, country)); + } + + List users = await _userCollection + .Find(Builders.Filter.Or(filters)) + .ToListAsync(cancellationToken); + + return users.ToLookup(t => t.Country); + } + + public Task GetUserAsync(ObjectId userId, CancellationToken cancellationToken) + { + return _userCollection.Find(c => c.Id == userId) + .FirstOrDefaultAsync(cancellationToken); + } + + public Task CreateUserAsync(User user, CancellationToken cancellationToken) + { + return _userCollection.InsertOneAsync(user, new InsertOneOptions(), cancellationToken); + } + + public async Task> GetUsersAsync( + IReadOnlyCollection userIds, + CancellationToken cancellationToken) + { + var filters = new List>(); + foreach (ObjectId userId in userIds) + { + filters.Add(Builders.Filter.Eq(u => u.Id, userId)); + } + + List users = await _userCollection + .Find(Builders.Filter.Or(filters)) + .ToListAsync(cancellationToken); + + return users.ToDictionary(t => t.Id); + } + } +} diff --git a/moscow_dotnext_2019/4_dataloader/UserType.cs b/moscow_dotnext_2019/4_dataloader/UserType.cs new file mode 100644 index 0000000..7af7a05 --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/UserType.cs @@ -0,0 +1,14 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class UserType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Name).Type>(); + } + } +} diff --git a/moscow_dotnext_2019/4_dataloader/global.json b/moscow_dotnext_2019/4_dataloader/global.json new file mode 100644 index 0000000..89b3b0f --- /dev/null +++ b/moscow_dotnext_2019/4_dataloader/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "2.2.402" + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/.vscode/launch.json b/moscow_dotnext_2019/5_persisted_queries/.vscode/launch.json new file mode 100644 index 0000000..d3f889e --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/StarWars/bin/Debug/netcoreapp3.0/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/moscow_dotnext_2019/5_persisted_queries/.vscode/tasks.json b/moscow_dotnext_2019/5_persisted_queries/.vscode/tasks.json new file mode 100644 index 0000000..383f2db --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/.vscode/tasks.json @@ -0,0 +1,25 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + "StarWars", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/.vscode/launch.json b/moscow_dotnext_2019/5_persisted_queries/StarWars/.vscode/launch.json new file mode 100644 index 0000000..401f807 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.1/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/.vscode/tasks.json b/moscow_dotnext_2019/5_persisted_queries/StarWars/.vscode/tasks.json new file mode 100644 index 0000000..68ae5ff --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet build", + "type": "shell", + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/AspNetCore.StarWars.csproj b/moscow_dotnext_2019/5_persisted_queries/StarWars/AspNetCore.StarWars.csproj new file mode 100644 index 0000000..841febc --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/AspNetCore.StarWars.csproj @@ -0,0 +1,32 @@ + + + + netcoreapp3.0 + false + 7.2 + true + $(NoWarn);1591 + + + + portable + true + + + + pdbonly + true + + + + + + + + + + + + + + diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Data/CharacterRepository.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Data/CharacterRepository.cs new file mode 100644 index 0000000..dd09134 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Data/CharacterRepository.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StarWars.Models; + +namespace StarWars.Data +{ + public class CharacterRepository + { + private Dictionary _characters; + private Dictionary _starships; + + public CharacterRepository() + { + _characters = CreateCharacters().ToDictionary(t => t.Id); + _starships = CreateStarships().ToDictionary(t => t.Id); + } + + public ICharacter GetHero(Episode episode) + { + if (episode == Episode.Empire) + { + return _characters["1000"]; + } + return _characters["2001"]; + } + + public ICharacter GetCharacter(string id) + { + if (_characters.TryGetValue(id, out ICharacter c)) + { + return c; + } + return null; + } + + public Human GetHuman(string id) + { + if (_characters.TryGetValue(id, out ICharacter c) + && c is Human h) + { + return h; + } + return null; + } + + public Droid GetDroid(string id) + { + if (_characters.TryGetValue(id, out ICharacter c) + && c is Droid d) + { + return d; + } + return null; + } + + public IEnumerable Search(string text) + { +#if ASPNETCLASSIC + IEnumerable filteredCharacters = _characters.Values + .Where(t => t.Name.Contains(text)); +#else + IEnumerable filteredCharacters = _characters.Values + .Where(t => t.Name.Contains(text, + StringComparison.OrdinalIgnoreCase)); +#endif + + foreach (ICharacter character in filteredCharacters) + { + yield return character; + } + +#if ASPNETCLASSIC + IEnumerable filteredStarships = _starships.Values + .Where(t => t.Name.Contains(text)); +#else + IEnumerable filteredStarships = _starships.Values + .Where(t => t.Name.Contains(text, + StringComparison.OrdinalIgnoreCase)); +#endif + + foreach (Starship starship in filteredStarships) + { + yield return starship; + } + } + + private static IEnumerable CreateCharacters() + { + yield return new Human + { + Id = "1000", + Name = "Luke Skywalker", + Friends = new[] { "1002", "1003", "2000", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Tatooine" + }; + + yield return new Human + { + Id = "1001", + Name = "Darth Vader", + Friends = new[] { "1004" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Tatooine" + }; + + yield return new Human + { + Id = "1002", + Name = "Han Solo", + Friends = new[] { "1000", "1003", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi } + }; + + yield return new Human + { + Id = "1003", + Name = "Leia Organa", + Friends = new[] { "1000", "1002", "2000", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Alderaan" + }; + + yield return new Human + { + Id = "1004", + Name = "Wilhuff Tarkin", + Friends = new[] { "1001" }, + AppearsIn = new[] { Episode.NewHope } + }; + + yield return new Droid + { + Id = "2000", + Name = "C-3PO", + Friends = new[] { "1000", "1002", "1003", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + PrimaryFunction = "Protocol" + }; + + yield return new Droid + { + Id = "2001", + Name = "R2-D2", + Friends = new[] { "1000", "1002", "1003" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + PrimaryFunction = "Astromech" + }; + } + + private static IEnumerable CreateStarships() + { + yield return new Starship + { + Id = "3000", + Name = "TIE Advanced x1", + Length = 9.2 + }; + } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Data/ReviewRepository.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Data/ReviewRepository.cs new file mode 100644 index 0000000..a586306 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Data/ReviewRepository.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using StarWars.Models; + +namespace StarWars.Data +{ + public class ReviewRepository + { + private readonly Dictionary> _data = + new Dictionary>(); + + public void AddReview(Episode episode, Review review) + { + if (!_data.TryGetValue(episode, out List reviews)) + { + reviews = new List(); + _data[episode] = reviews; + } + + reviews.Add(review); + } + + public IEnumerable GetReviews(Episode episode) + { + if (_data.TryGetValue(episode, out List reviews)) + { + return reviews; + } + return Array.Empty(); + } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Droid.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Droid.cs new file mode 100644 index 0000000..0ee4213 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Droid.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A droid in the Star Wars universe. + /// + public class Droid + : ICharacter + { + /// + public string Id { get; set; } + + /// + public string Name { get; set; } + + /// + public IReadOnlyList Friends { get; set; } + + /// + public IReadOnlyList AppearsIn { get; set; } + + /// + /// The droid's primary function. + /// + public string PrimaryFunction { get; set; } + + /// + public double Height { get; } = 1.72d; + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Episode.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Episode.cs new file mode 100644 index 0000000..6900cf6 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Episode.cs @@ -0,0 +1,21 @@ +namespace StarWars.Models +{ + /// + /// The Star Wars episodes. + /// + public enum Episode + { + /// + /// Star Wars Episode IV: A New Hope + /// + NewHope, + /// + /// Star Wars Episode V: Empire Strikes Back + /// + Empire, + /// + /// Star Wars Episode VI: Return of the Jedi + /// + Jedi + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Human.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Human.cs new file mode 100644 index 0000000..caf6021 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Human.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A human character in the Star Wars universe. + /// + public class Human + : ICharacter + { + /// + public string Id { get; set; } + + /// + public string Name { get; set; } + + /// + public IReadOnlyList Friends { get; set; } + + /// + public IReadOnlyList AppearsIn { get; set; } + + /// + /// The planet the character is originally from. + /// + public string HomePlanet { get; set; } + + /// + public double Height { get; } = 1.72d; + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/ICharacter.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/ICharacter.cs new file mode 100644 index 0000000..f9186c1 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/ICharacter.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A character in the Star Wars universe. + /// + public interface ICharacter + { + /// + /// The unique identifier for the character. + /// + string Id { get; } + + /// + /// The name of the character. + /// + string Name { get; } + + /// + /// The names of the character's friends. + /// + IReadOnlyList Friends { get; } + + /// + /// The episodes the character appears in. + /// + IReadOnlyList AppearsIn { get; } + + /// + /// The height of the character. + /// + double Height { get; } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Review.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Review.cs new file mode 100644 index 0000000..3f18f16 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Review.cs @@ -0,0 +1,18 @@ +namespace StarWars.Models +{ + /// + /// A review of a particular movie. + /// + public class Review + { + /// + /// The number of stars given for this review. + /// + public int Stars { get; set; } + + /// + /// An explanation for the rating. + /// + public string Commentary { get; set; } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Starship.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Starship.cs new file mode 100644 index 0000000..5d7c241 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Starship.cs @@ -0,0 +1,23 @@ +namespace StarWars.Models +{ + /// + /// A starship in the Star Wars universe. + /// + public class Starship + { + /// + /// The Id of the starship. + /// + public string Id { get; set; } + + /// + /// The name of the starship. + /// + public string Name { get; set; } + + /// + /// The length of the starship. + /// + public double Length { get; set; } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Unit.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Unit.cs new file mode 100644 index 0000000..15316aa --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Models/Unit.cs @@ -0,0 +1,11 @@ +namespace StarWars.Models +{ + /// + /// Different units of measurement. + /// + public enum Unit + { + Foot, + Meters + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Mutation.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Mutation.cs new file mode 100644 index 0000000..45d4adc --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Mutation.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Subscriptions; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Mutation + { + private readonly ReviewRepository _repository; + + public Mutation(ReviewRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Creates a review for a given Star Wars episode. + /// + /// The episode to review. + /// The review. + /// The event sending service. + /// The created review. + public async Task CreateReview( + Episode episode, Review review, + [Service]IEventSender eventSender) + { + _repository.AddReview(episode, review); + await eventSender.SendAsync(new OnReviewMessage(episode, review)); + return review; + } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/OnReviewMessage.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/OnReviewMessage.cs new file mode 100644 index 0000000..95f9cff --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/OnReviewMessage.cs @@ -0,0 +1,23 @@ +using HotChocolate.Language; +using HotChocolate.Subscriptions; +using StarWars.Models; + +namespace StarWars +{ + public class OnReviewMessage + : EventMessage + { + public OnReviewMessage(Episode episode, Review review) + : base(CreateEventDescription(episode), review) + { + } + + private static EventDescription CreateEventDescription(Episode episode) + { + return new EventDescription("onReview", + new ArgumentNode("episode", + new EnumValueNode( + episode.ToString().ToUpperInvariant()))); + } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Program.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Program.cs new file mode 100644 index 0000000..0358799 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace StarWars +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Query.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Query.cs new file mode 100644 index 0000000..887534e --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Query.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using HotChocolate.Resolvers; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Query + { + private readonly CharacterRepository _repository; + + public Query(CharacterRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Retrieve a hero by a particular Star Wars episode. + /// + /// The episode to look up by. + /// The character. + public ICharacter GetHero(Episode episode) + { + return _repository.GetHero(episode); + } + + /// + /// Gets a human by Id. + /// + /// The Id of the human to retrieve. + /// The human. + public Human GetHuman(string id) + { + return _repository.GetHuman(id); + } + + /// + /// Get a particular droid by Id. + /// + /// The Id of the droid. + /// The droid. + public Droid GetDroid(string id) + { + return _repository.GetDroid(id); + } + + public IEnumerable GetCharacter(string[] characterIds, IResolverContext context) + { + foreach (string characterId in characterIds) + { + ICharacter character = _repository.GetCharacter(characterId); + if (character == null) + { + context.ReportError( + "Could not resolve a charachter for the " + + $"character-id {characterId}."); + } + else + { + yield return character; + } + } + } + + public IEnumerable Search(string text) + { + return _repository.Search(text); + } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Resolvers/SharedResolvers.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Resolvers/SharedResolvers.cs new file mode 100644 index 0000000..2b147d8 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Resolvers/SharedResolvers.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using HotChocolate; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars.Resolvers +{ + public class SharedResolvers + { + [System.Obsolete] + public IEnumerable GetCharacter( + [Parent]ICharacter character, + [Service]CharacterRepository repository) + { + foreach (string friendId in character.Friends) + { + ICharacter friend = repository.GetCharacter(friendId); + if (friend != null) + { + yield return friend; + } + } + } + + public double GetHeight(Unit? unit, [Parent]ICharacter character) + => ConvertToUnit(character.Height, unit); + + public double GetLength(Unit? unit, [Parent]Starship starship) + => ConvertToUnit(starship.Length, unit); + + private double ConvertToUnit(double length, Unit? unit) + { + if (unit == Unit.Foot) + { + return length * 3.28084d; + } + return length; + } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Startup.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Startup.cs new file mode 100644 index 0000000..2bab6bb --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Startup.cs @@ -0,0 +1,50 @@ +using HotChocolate; +using HotChocolate.AspNetCore; +using HotChocolate.Execution; +using HotChocolate.Subscriptions; +using HotChocolate.PersistedQueries.FileSystem; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using StarWars.Data; +using StarWars.Types; + +namespace StarWars +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + // Add the custom services like repositories etc ... + services.AddSingleton(); + services.AddSingleton(); + + // Add in-memory event provider + services.AddInMemorySubscriptionProvider(); + + services.AddReadOnlyFileSystemQueryStorage("./queries"); + + // Add GraphQL Services + services.AddGraphQL(sp => SchemaBuilder.New() + .AddServices(sp) + .AddQueryType() + .AddMutationType() + .AddSubscriptionType() + .AddType() + .AddType() + .AddType() + .Create(), + builder => builder.UsePersistedQueryPipeline()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app + .UseWebSockets() + .UseGraphQL("/graphql"); + } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Subscription.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Subscription.cs new file mode 100644 index 0000000..06a88b2 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Subscription.cs @@ -0,0 +1,23 @@ +using System; +using HotChocolate.Subscriptions; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Subscription + { + private readonly ReviewRepository _repository; + + public Subscription(ReviewRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + public Review OnReview(Episode episode, IEventMessage message) + { + return (Review)message.Payload; + } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/CharacterType.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/CharacterType.cs new file mode 100644 index 0000000..b9c5c98 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/CharacterType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; + +namespace StarWars.Types +{ + public class CharacterType + : InterfaceType + { + protected override void Configure(IInterfaceTypeDescriptor descriptor) + { + descriptor.Name("Character"); + + descriptor.Field(f => f.Id) + .Type>(); + + descriptor.Field(f => f.Name) + .Type(); + + descriptor.Field(f => f.Friends) + .UsePaging(); + + descriptor.Field(f => f.AppearsIn) + .Type>(); + + descriptor.Field(f => f.Height) + .Type() + .Argument("unit", a => a.Type>()); + } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/DroidType.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/DroidType.cs new file mode 100644 index 0000000..c34fe9a --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/DroidType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class DroidType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.AppearsIn) + .Type>(); + + descriptor.Field(r => r.GetCharacter(default, default)) + .UsePaging() + .Name("friends"); + + descriptor.Field(t => t.GetHeight(default, default)) + .Type() + .Argument("unit", a => a.Type>()) + .Name("height"); + } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/EpisodeType.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/EpisodeType.cs new file mode 100644 index 0000000..bfb484d --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/EpisodeType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class EpisodeType + : EnumType + { + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/HumanType.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/HumanType.cs new file mode 100644 index 0000000..3ab1c6a --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/HumanType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class HumanType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.AppearsIn) + .Type>(); + + descriptor.Field(r => r.GetCharacter(default, default)) + .UsePaging() + .Name("friends"); + + descriptor.Field(t => t.GetHeight(default, default)) + .Type() + .Argument("unit", a => a.Type>()) + .Name("height"); + } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/MutationType.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/MutationType.cs new file mode 100644 index 0000000..71963b7 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/MutationType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Types; + +namespace StarWars.Types +{ + public class MutationType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.CreateReview(default, default, default)) + .Type>() + .Argument("episode", a => a.Type>()) + .Argument("review", a => a.Type>()); + } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/QueryType.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/QueryType.cs new file mode 100644 index 0000000..193cf89 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/QueryType.cs @@ -0,0 +1,22 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class QueryType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetHero(default)) + .Type() + .Argument("episode", a => a.DefaultValue(Episode.NewHope)); + + descriptor.Field(t => t.GetCharacter(default, default)) + .Type>>>(); + + descriptor.Field(t => t.Search(default)) + .Type>(); + } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/ReviewInputType.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/ReviewInputType.cs new file mode 100644 index 0000000..300fbe6 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/ReviewInputType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class ReviewInputType + : InputObjectType + { + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/ReviewType.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/ReviewType.cs new file mode 100644 index 0000000..be94b38 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/ReviewType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class ReviewType + : ObjectType + { + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/SearchResultType.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/SearchResultType.cs new file mode 100644 index 0000000..7d1bb88 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/SearchResultType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class SearchResultType + : UnionType + { + protected override void Configure(IUnionTypeDescriptor descriptor) + { + descriptor.Name("SearchResult"); + descriptor.Type>(); + descriptor.Type(); + descriptor.Type(); + } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/StarshipType.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/StarshipType.cs new file mode 100644 index 0000000..7ad6b10 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/StarshipType.cs @@ -0,0 +1,18 @@ +using HotChocolate.Types; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class StarshipType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.GetLength(default, default)); + } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/SubscriptionType.cs b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/SubscriptionType.cs new file mode 100644 index 0000000..ed872e5 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/Types/SubscriptionType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace StarWars.Types +{ + public class SubscriptionType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.OnReview(default, default)) + .Type>() + .Argument("episode", arg => arg.Type>()); + } + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/StarWars/queries/foo b/moscow_dotnext_2019/5_persisted_queries/StarWars/queries/foo new file mode 100644 index 0000000..985c3eb --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/StarWars/queries/foo @@ -0,0 +1,5 @@ +query foo { + hero(episode: NEWHOPE) { + name + } +} diff --git a/moscow_dotnext_2019/5_persisted_queries/global.json b/moscow_dotnext_2019/5_persisted_queries/global.json new file mode 100644 index 0000000..79422f0 --- /dev/null +++ b/moscow_dotnext_2019/5_persisted_queries/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "3.0.100" + } +} diff --git a/moscow_dotnext_2019/GraphQL_Intro_DN.pptx b/moscow_dotnext_2019/GraphQL_Intro_DN.pptx new file mode 100644 index 0000000..b51218e Binary files /dev/null and b/moscow_dotnext_2019/GraphQL_Intro_DN.pptx differ diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..375d64d --- /dev/null +++ b/nuget.config @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/oxford_usergroup_2019/1_graphql_vs_rest/index.html b/oxford_usergroup_2019/1_graphql_vs_rest/index.html new file mode 100644 index 0000000..889c94b --- /dev/null +++ b/oxford_usergroup_2019/1_graphql_vs_rest/index.html @@ -0,0 +1,268 @@ + + + + + GraphQL vs. REST + + + +
      + + vs. + +
      +
      + Loading... +
      +
      +

      + An unsorted list of characters who played in the same films where Luke + Skywalker were present +

      +

      + Used and returned + characters which took + ms. +

      +
        +
        + + + diff --git a/oxford_usergroup_2019/2_hello_world/.vscode/launch.json b/oxford_usergroup_2019/2_hello_world/.vscode/launch.json new file mode 100644 index 0000000..c509473 --- /dev/null +++ b/oxford_usergroup_2019/2_hello_world/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp3.0/2_hello_world.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/oxford_usergroup_2019/2_hello_world/.vscode/tasks.json b/oxford_usergroup_2019/2_hello_world/.vscode/tasks.json new file mode 100644 index 0000000..31c32bd --- /dev/null +++ b/oxford_usergroup_2019/2_hello_world/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/oxford_usergroup_2019/2_hello_world/2_hello_world.csproj b/oxford_usergroup_2019/2_hello_world/2_hello_world.csproj new file mode 100644 index 0000000..b3d059f --- /dev/null +++ b/oxford_usergroup_2019/2_hello_world/2_hello_world.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.0 + _2_hello_world + + + + + + + diff --git a/oxford_usergroup_2019/2_hello_world/Program.cs b/oxford_usergroup_2019/2_hello_world/Program.cs new file mode 100644 index 0000000..7a86352 --- /dev/null +++ b/oxford_usergroup_2019/2_hello_world/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace HelloWorld +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/oxford_usergroup_2019/2_hello_world/Startup.cs b/oxford_usergroup_2019/2_hello_world/Startup.cs new file mode 100644 index 0000000..4fc3366 --- /dev/null +++ b/oxford_usergroup_2019/2_hello_world/Startup.cs @@ -0,0 +1,41 @@ +using HotChocolate; +using HotChocolate.AspNetCore; +using HotChocolate.Types; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace HelloWorld +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddGraphQL(SchemaBuilder.New() + .AddQueryType() + .Create()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseGraphQL(); + } + } + + public class QueryType : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Hello()).Type>(); + } + } + + public class Query + { + public string Hello() => null; + } +} diff --git a/oxford_usergroup_2019/2_hello_world/appsettings.Development.json b/oxford_usergroup_2019/2_hello_world/appsettings.Development.json new file mode 100644 index 0000000..a2880cb --- /dev/null +++ b/oxford_usergroup_2019/2_hello_world/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/oxford_usergroup_2019/2_hello_world/appsettings.json b/oxford_usergroup_2019/2_hello_world/appsettings.json new file mode 100644 index 0000000..81ff877 --- /dev/null +++ b/oxford_usergroup_2019/2_hello_world/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/oxford_usergroup_2019/2_hello_world/copy.txt b/oxford_usergroup_2019/2_hello_world/copy.txt new file mode 100644 index 0000000..e23c42a --- /dev/null +++ b/oxford_usergroup_2019/2_hello_world/copy.txt @@ -0,0 +1,10 @@ +public class Query +{ + public string Hello() => "World"; +} + +services.AddGraphQL(sp => SchemaBuilder.New() + .AddQueryType() + .Create()); + +app.UseGraphQL(); \ No newline at end of file diff --git a/oxford_usergroup_2019/2_hello_world/global.json b/oxford_usergroup_2019/2_hello_world/global.json new file mode 100644 index 0000000..79422f0 --- /dev/null +++ b/oxford_usergroup_2019/2_hello_world/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "3.0.100" + } +} diff --git a/oxford_usergroup_2019/3_operations/.vscode/launch.json b/oxford_usergroup_2019/3_operations/.vscode/launch.json new file mode 100644 index 0000000..d3f889e --- /dev/null +++ b/oxford_usergroup_2019/3_operations/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/StarWars/bin/Debug/netcoreapp3.0/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/oxford_usergroup_2019/3_operations/.vscode/tasks.json b/oxford_usergroup_2019/3_operations/.vscode/tasks.json new file mode 100644 index 0000000..383f2db --- /dev/null +++ b/oxford_usergroup_2019/3_operations/.vscode/tasks.json @@ -0,0 +1,25 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + "StarWars", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/oxford_usergroup_2019/3_operations/StarWars/.vscode/launch.json b/oxford_usergroup_2019/3_operations/StarWars/.vscode/launch.json new file mode 100644 index 0000000..401f807 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.1/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/oxford_usergroup_2019/3_operations/StarWars/.vscode/tasks.json b/oxford_usergroup_2019/3_operations/StarWars/.vscode/tasks.json new file mode 100644 index 0000000..68ae5ff --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet build", + "type": "shell", + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/AspNetCore.StarWars.csproj b/oxford_usergroup_2019/3_operations/StarWars/AspNetCore.StarWars.csproj new file mode 100644 index 0000000..1b041de --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/AspNetCore.StarWars.csproj @@ -0,0 +1,34 @@ + + + + netcoreapp3.0 + false + 7.2 + true + $(NoWarn);1591 + + + + portable + true + + + + pdbonly + true + + + + + + + + + + + + + + + + diff --git a/oxford_usergroup_2019/3_operations/StarWars/Data/CharacterRepository.cs b/oxford_usergroup_2019/3_operations/StarWars/Data/CharacterRepository.cs new file mode 100644 index 0000000..dd09134 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Data/CharacterRepository.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StarWars.Models; + +namespace StarWars.Data +{ + public class CharacterRepository + { + private Dictionary _characters; + private Dictionary _starships; + + public CharacterRepository() + { + _characters = CreateCharacters().ToDictionary(t => t.Id); + _starships = CreateStarships().ToDictionary(t => t.Id); + } + + public ICharacter GetHero(Episode episode) + { + if (episode == Episode.Empire) + { + return _characters["1000"]; + } + return _characters["2001"]; + } + + public ICharacter GetCharacter(string id) + { + if (_characters.TryGetValue(id, out ICharacter c)) + { + return c; + } + return null; + } + + public Human GetHuman(string id) + { + if (_characters.TryGetValue(id, out ICharacter c) + && c is Human h) + { + return h; + } + return null; + } + + public Droid GetDroid(string id) + { + if (_characters.TryGetValue(id, out ICharacter c) + && c is Droid d) + { + return d; + } + return null; + } + + public IEnumerable Search(string text) + { +#if ASPNETCLASSIC + IEnumerable filteredCharacters = _characters.Values + .Where(t => t.Name.Contains(text)); +#else + IEnumerable filteredCharacters = _characters.Values + .Where(t => t.Name.Contains(text, + StringComparison.OrdinalIgnoreCase)); +#endif + + foreach (ICharacter character in filteredCharacters) + { + yield return character; + } + +#if ASPNETCLASSIC + IEnumerable filteredStarships = _starships.Values + .Where(t => t.Name.Contains(text)); +#else + IEnumerable filteredStarships = _starships.Values + .Where(t => t.Name.Contains(text, + StringComparison.OrdinalIgnoreCase)); +#endif + + foreach (Starship starship in filteredStarships) + { + yield return starship; + } + } + + private static IEnumerable CreateCharacters() + { + yield return new Human + { + Id = "1000", + Name = "Luke Skywalker", + Friends = new[] { "1002", "1003", "2000", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Tatooine" + }; + + yield return new Human + { + Id = "1001", + Name = "Darth Vader", + Friends = new[] { "1004" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Tatooine" + }; + + yield return new Human + { + Id = "1002", + Name = "Han Solo", + Friends = new[] { "1000", "1003", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi } + }; + + yield return new Human + { + Id = "1003", + Name = "Leia Organa", + Friends = new[] { "1000", "1002", "2000", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Alderaan" + }; + + yield return new Human + { + Id = "1004", + Name = "Wilhuff Tarkin", + Friends = new[] { "1001" }, + AppearsIn = new[] { Episode.NewHope } + }; + + yield return new Droid + { + Id = "2000", + Name = "C-3PO", + Friends = new[] { "1000", "1002", "1003", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + PrimaryFunction = "Protocol" + }; + + yield return new Droid + { + Id = "2001", + Name = "R2-D2", + Friends = new[] { "1000", "1002", "1003" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + PrimaryFunction = "Astromech" + }; + } + + private static IEnumerable CreateStarships() + { + yield return new Starship + { + Id = "3000", + Name = "TIE Advanced x1", + Length = 9.2 + }; + } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Data/ReviewRepository.cs b/oxford_usergroup_2019/3_operations/StarWars/Data/ReviewRepository.cs new file mode 100644 index 0000000..a586306 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Data/ReviewRepository.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using StarWars.Models; + +namespace StarWars.Data +{ + public class ReviewRepository + { + private readonly Dictionary> _data = + new Dictionary>(); + + public void AddReview(Episode episode, Review review) + { + if (!_data.TryGetValue(episode, out List reviews)) + { + reviews = new List(); + _data[episode] = reviews; + } + + reviews.Add(review); + } + + public IEnumerable GetReviews(Episode episode) + { + if (_data.TryGetValue(episode, out List reviews)) + { + return reviews; + } + return Array.Empty(); + } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Models/Droid.cs b/oxford_usergroup_2019/3_operations/StarWars/Models/Droid.cs new file mode 100644 index 0000000..0ee4213 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Models/Droid.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A droid in the Star Wars universe. + /// + public class Droid + : ICharacter + { + /// + public string Id { get; set; } + + /// + public string Name { get; set; } + + /// + public IReadOnlyList Friends { get; set; } + + /// + public IReadOnlyList AppearsIn { get; set; } + + /// + /// The droid's primary function. + /// + public string PrimaryFunction { get; set; } + + /// + public double Height { get; } = 1.72d; + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Models/Episode.cs b/oxford_usergroup_2019/3_operations/StarWars/Models/Episode.cs new file mode 100644 index 0000000..6900cf6 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Models/Episode.cs @@ -0,0 +1,21 @@ +namespace StarWars.Models +{ + /// + /// The Star Wars episodes. + /// + public enum Episode + { + /// + /// Star Wars Episode IV: A New Hope + /// + NewHope, + /// + /// Star Wars Episode V: Empire Strikes Back + /// + Empire, + /// + /// Star Wars Episode VI: Return of the Jedi + /// + Jedi + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Models/Human.cs b/oxford_usergroup_2019/3_operations/StarWars/Models/Human.cs new file mode 100644 index 0000000..caf6021 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Models/Human.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A human character in the Star Wars universe. + /// + public class Human + : ICharacter + { + /// + public string Id { get; set; } + + /// + public string Name { get; set; } + + /// + public IReadOnlyList Friends { get; set; } + + /// + public IReadOnlyList AppearsIn { get; set; } + + /// + /// The planet the character is originally from. + /// + public string HomePlanet { get; set; } + + /// + public double Height { get; } = 1.72d; + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Models/ICharacter.cs b/oxford_usergroup_2019/3_operations/StarWars/Models/ICharacter.cs new file mode 100644 index 0000000..f9186c1 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Models/ICharacter.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A character in the Star Wars universe. + /// + public interface ICharacter + { + /// + /// The unique identifier for the character. + /// + string Id { get; } + + /// + /// The name of the character. + /// + string Name { get; } + + /// + /// The names of the character's friends. + /// + IReadOnlyList Friends { get; } + + /// + /// The episodes the character appears in. + /// + IReadOnlyList AppearsIn { get; } + + /// + /// The height of the character. + /// + double Height { get; } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Models/Review.cs b/oxford_usergroup_2019/3_operations/StarWars/Models/Review.cs new file mode 100644 index 0000000..3f18f16 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Models/Review.cs @@ -0,0 +1,18 @@ +namespace StarWars.Models +{ + /// + /// A review of a particular movie. + /// + public class Review + { + /// + /// The number of stars given for this review. + /// + public int Stars { get; set; } + + /// + /// An explanation for the rating. + /// + public string Commentary { get; set; } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Models/Starship.cs b/oxford_usergroup_2019/3_operations/StarWars/Models/Starship.cs new file mode 100644 index 0000000..5d7c241 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Models/Starship.cs @@ -0,0 +1,23 @@ +namespace StarWars.Models +{ + /// + /// A starship in the Star Wars universe. + /// + public class Starship + { + /// + /// The Id of the starship. + /// + public string Id { get; set; } + + /// + /// The name of the starship. + /// + public string Name { get; set; } + + /// + /// The length of the starship. + /// + public double Length { get; set; } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Models/Unit.cs b/oxford_usergroup_2019/3_operations/StarWars/Models/Unit.cs new file mode 100644 index 0000000..15316aa --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Models/Unit.cs @@ -0,0 +1,11 @@ +namespace StarWars.Models +{ + /// + /// Different units of measurement. + /// + public enum Unit + { + Foot, + Meters + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Mutation.cs b/oxford_usergroup_2019/3_operations/StarWars/Mutation.cs new file mode 100644 index 0000000..45d4adc --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Mutation.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Subscriptions; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Mutation + { + private readonly ReviewRepository _repository; + + public Mutation(ReviewRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Creates a review for a given Star Wars episode. + /// + /// The episode to review. + /// The review. + /// The event sending service. + /// The created review. + public async Task CreateReview( + Episode episode, Review review, + [Service]IEventSender eventSender) + { + _repository.AddReview(episode, review); + await eventSender.SendAsync(new OnReviewMessage(episode, review)); + return review; + } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/OnReviewMessage.cs b/oxford_usergroup_2019/3_operations/StarWars/OnReviewMessage.cs new file mode 100644 index 0000000..95f9cff --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/OnReviewMessage.cs @@ -0,0 +1,23 @@ +using HotChocolate.Language; +using HotChocolate.Subscriptions; +using StarWars.Models; + +namespace StarWars +{ + public class OnReviewMessage + : EventMessage + { + public OnReviewMessage(Episode episode, Review review) + : base(CreateEventDescription(episode), review) + { + } + + private static EventDescription CreateEventDescription(Episode episode) + { + return new EventDescription("onReview", + new ArgumentNode("episode", + new EnumValueNode( + episode.ToString().ToUpperInvariant()))); + } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Program.cs b/oxford_usergroup_2019/3_operations/StarWars/Program.cs new file mode 100644 index 0000000..0358799 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace StarWars +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Query.cs b/oxford_usergroup_2019/3_operations/StarWars/Query.cs new file mode 100644 index 0000000..887534e --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Query.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using HotChocolate.Resolvers; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Query + { + private readonly CharacterRepository _repository; + + public Query(CharacterRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Retrieve a hero by a particular Star Wars episode. + /// + /// The episode to look up by. + /// The character. + public ICharacter GetHero(Episode episode) + { + return _repository.GetHero(episode); + } + + /// + /// Gets a human by Id. + /// + /// The Id of the human to retrieve. + /// The human. + public Human GetHuman(string id) + { + return _repository.GetHuman(id); + } + + /// + /// Get a particular droid by Id. + /// + /// The Id of the droid. + /// The droid. + public Droid GetDroid(string id) + { + return _repository.GetDroid(id); + } + + public IEnumerable GetCharacter(string[] characterIds, IResolverContext context) + { + foreach (string characterId in characterIds) + { + ICharacter character = _repository.GetCharacter(characterId); + if (character == null) + { + context.ReportError( + "Could not resolve a charachter for the " + + $"character-id {characterId}."); + } + else + { + yield return character; + } + } + } + + public IEnumerable Search(string text) + { + return _repository.Search(text); + } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Resolvers/SharedResolvers.cs b/oxford_usergroup_2019/3_operations/StarWars/Resolvers/SharedResolvers.cs new file mode 100644 index 0000000..3911604 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Resolvers/SharedResolvers.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using HotChocolate; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars.Resolvers +{ + public class SharedResolvers + { + public IEnumerable GetCharacter( + [Parent]ICharacter character, + [Service]CharacterRepository repository) + { + foreach (string friendId in character.Friends) + { + ICharacter friend = repository.GetCharacter(friendId); + if (friend != null) + { + yield return friend; + } + } + } + + public double GetHeight(Unit? unit, [Parent]ICharacter character) + => ConvertToUnit(character.Height, unit); + + public double GetLength(Unit? unit, [Parent]Starship starship) + => ConvertToUnit(starship.Length, unit); + + private double ConvertToUnit(double length, Unit? unit) + { + if (unit == Unit.Foot) + { + return length * 3.28084d; + } + return length; + } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Startup.cs b/oxford_usergroup_2019/3_operations/StarWars/Startup.cs new file mode 100644 index 0000000..b55e4d8 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Startup.cs @@ -0,0 +1,66 @@ +using System.Security.Claims; +using HotChocolate; +using HotChocolate.AspNetCore; +using HotChocolate.AspNetCore.Voyager; +using HotChocolate.Execution.Configuration; +using HotChocolate.Subscriptions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using StarWars.Data; +using StarWars.Types; + +namespace StarWars +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + // Add the custom services like repositories etc ... + services.AddSingleton(); + services.AddSingleton(); + + // Add in-memory event provider + services.AddInMemorySubscriptionProvider(); + + // Add GraphQL Services + services.AddGraphQL(sp => SchemaBuilder.New() + .AddServices(sp) + + // Adds the authorize directive and + // enable the authorization middleware. + .AddAuthorizeDirectiveType() + + .AddQueryType() + .AddMutationType() + .AddSubscriptionType() + .AddType() + .AddType() + .AddType() + .Create()); + + + // Add Authorization Policy + services.AddAuthorization(options => + { + options.AddPolicy("HasCountry", policy => + policy.RequireAssertion(context => + context.User.HasClaim(c => + (c.Type == ClaimTypes.Country)))); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app + .UseWebSockets() + .UseGraphQL("/graphql") + .UseGraphiQL("/graphql") + .UsePlayground("/graphql") + .UseVoyager("/graphql"); + } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Subscription.cs b/oxford_usergroup_2019/3_operations/StarWars/Subscription.cs new file mode 100644 index 0000000..06a88b2 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Subscription.cs @@ -0,0 +1,23 @@ +using System; +using HotChocolate.Subscriptions; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Subscription + { + private readonly ReviewRepository _repository; + + public Subscription(ReviewRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + public Review OnReview(Episode episode, IEventMessage message) + { + return (Review)message.Payload; + } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Types/CharacterType.cs b/oxford_usergroup_2019/3_operations/StarWars/Types/CharacterType.cs new file mode 100644 index 0000000..b9c5c98 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Types/CharacterType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; + +namespace StarWars.Types +{ + public class CharacterType + : InterfaceType + { + protected override void Configure(IInterfaceTypeDescriptor descriptor) + { + descriptor.Name("Character"); + + descriptor.Field(f => f.Id) + .Type>(); + + descriptor.Field(f => f.Name) + .Type(); + + descriptor.Field(f => f.Friends) + .UsePaging(); + + descriptor.Field(f => f.AppearsIn) + .Type>(); + + descriptor.Field(f => f.Height) + .Type() + .Argument("unit", a => a.Type>()); + } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Types/DroidType.cs b/oxford_usergroup_2019/3_operations/StarWars/Types/DroidType.cs new file mode 100644 index 0000000..c34fe9a --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Types/DroidType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class DroidType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.AppearsIn) + .Type>(); + + descriptor.Field(r => r.GetCharacter(default, default)) + .UsePaging() + .Name("friends"); + + descriptor.Field(t => t.GetHeight(default, default)) + .Type() + .Argument("unit", a => a.Type>()) + .Name("height"); + } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Types/EpisodeType.cs b/oxford_usergroup_2019/3_operations/StarWars/Types/EpisodeType.cs new file mode 100644 index 0000000..bfb484d --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Types/EpisodeType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class EpisodeType + : EnumType + { + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Types/HumanType.cs b/oxford_usergroup_2019/3_operations/StarWars/Types/HumanType.cs new file mode 100644 index 0000000..3ab1c6a --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Types/HumanType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class HumanType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.AppearsIn) + .Type>(); + + descriptor.Field(r => r.GetCharacter(default, default)) + .UsePaging() + .Name("friends"); + + descriptor.Field(t => t.GetHeight(default, default)) + .Type() + .Argument("unit", a => a.Type>()) + .Name("height"); + } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Types/MutationType.cs b/oxford_usergroup_2019/3_operations/StarWars/Types/MutationType.cs new file mode 100644 index 0000000..71963b7 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Types/MutationType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Types; + +namespace StarWars.Types +{ + public class MutationType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.CreateReview(default, default, default)) + .Type>() + .Argument("episode", a => a.Type>()) + .Argument("review", a => a.Type>()); + } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Types/QueryType.cs b/oxford_usergroup_2019/3_operations/StarWars/Types/QueryType.cs new file mode 100644 index 0000000..193cf89 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Types/QueryType.cs @@ -0,0 +1,22 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class QueryType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetHero(default)) + .Type() + .Argument("episode", a => a.DefaultValue(Episode.NewHope)); + + descriptor.Field(t => t.GetCharacter(default, default)) + .Type>>>(); + + descriptor.Field(t => t.Search(default)) + .Type>(); + } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Types/ReviewInputType.cs b/oxford_usergroup_2019/3_operations/StarWars/Types/ReviewInputType.cs new file mode 100644 index 0000000..300fbe6 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Types/ReviewInputType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class ReviewInputType + : InputObjectType + { + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Types/ReviewType.cs b/oxford_usergroup_2019/3_operations/StarWars/Types/ReviewType.cs new file mode 100644 index 0000000..be94b38 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Types/ReviewType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class ReviewType + : ObjectType + { + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Types/SearchResultType.cs b/oxford_usergroup_2019/3_operations/StarWars/Types/SearchResultType.cs new file mode 100644 index 0000000..7d1bb88 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Types/SearchResultType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class SearchResultType + : UnionType + { + protected override void Configure(IUnionTypeDescriptor descriptor) + { + descriptor.Name("SearchResult"); + descriptor.Type>(); + descriptor.Type(); + descriptor.Type(); + } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Types/StarshipType.cs b/oxford_usergroup_2019/3_operations/StarWars/Types/StarshipType.cs new file mode 100644 index 0000000..7ad6b10 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Types/StarshipType.cs @@ -0,0 +1,18 @@ +using HotChocolate.Types; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class StarshipType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.GetLength(default, default)); + } + } +} diff --git a/oxford_usergroup_2019/3_operations/StarWars/Types/SubscriptionType.cs b/oxford_usergroup_2019/3_operations/StarWars/Types/SubscriptionType.cs new file mode 100644 index 0000000..ed872e5 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/StarWars/Types/SubscriptionType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace StarWars.Types +{ + public class SubscriptionType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.OnReview(default, default)) + .Type>() + .Argument("episode", arg => arg.Type>()); + } + } +} diff --git a/oxford_usergroup_2019/3_operations/global.json b/oxford_usergroup_2019/3_operations/global.json new file mode 100644 index 0000000..79422f0 --- /dev/null +++ b/oxford_usergroup_2019/3_operations/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "3.0.100" + } +} diff --git a/oxford_usergroup_2019/4_dataloader/DataLoader.csproj b/oxford_usergroup_2019/4_dataloader/DataLoader.csproj new file mode 100644 index 0000000..9c15e3e --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/DataLoader.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp2.2 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + + + \ No newline at end of file diff --git a/oxford_usergroup_2019/4_dataloader/DataLoader.sln b/oxford_usergroup_2019/4_dataloader/DataLoader.sln new file mode 100644 index 0000000..f90e842 --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/DataLoader.sln @@ -0,0 +1,17 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataLoader", "DataLoader.csproj", "{8B0F2085-502A-495C-BD9D-9827B8F3F0EE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8B0F2085-502A-495C-BD9D-9827B8F3F0EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B0F2085-502A-495C-BD9D-9827B8F3F0EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B0F2085-502A-495C-BD9D-9827B8F3F0EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B0F2085-502A-495C-BD9D-9827B8F3F0EE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/oxford_usergroup_2019/4_dataloader/Message.cs b/oxford_usergroup_2019/4_dataloader/Message.cs new file mode 100644 index 0000000..5a16590 --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/Message.cs @@ -0,0 +1,16 @@ +using System; +using MongoDB.Bson; +using HotChocolate.Language; + +namespace HotChocolate.Examples.Paging +{ + public class Message + { + public ObjectId Id { get; set; } + public string Text { get; set; } + public DateTimeOffset Created { get; set; } + public int Favorites { get; set; } + public ObjectId UserId { get; set; } + public ObjectId? ReplyToId { get; set; } + } +} diff --git a/oxford_usergroup_2019/4_dataloader/MessageInput.cs b/oxford_usergroup_2019/4_dataloader/MessageInput.cs new file mode 100644 index 0000000..22b7823 --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/MessageInput.cs @@ -0,0 +1,11 @@ +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class MessageInput + { + public string Text { get; set; } + public ObjectId UserId { get; set; } + public ObjectId? ReplyToId { get; set; } + } +} diff --git a/oxford_usergroup_2019/4_dataloader/MessageInputType.cs b/oxford_usergroup_2019/4_dataloader/MessageInputType.cs new file mode 100644 index 0000000..249be7d --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/MessageInputType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class MessageInputType + : InputObjectType + { + protected override void Configure(IInputObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Text).Type>(); + descriptor.Field(t => t.UserId).Type>(); + descriptor.Field(t => t.ReplyToId).Type(); + } + } +} diff --git a/oxford_usergroup_2019/4_dataloader/MessageRepository.cs b/oxford_usergroup_2019/4_dataloader/MessageRepository.cs new file mode 100644 index 0000000..ad224ae --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/MessageRepository.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace HotChocolate.Examples.Paging +{ + public class MessageRepository + { + private readonly IMongoCollection _messageCollection; + + public MessageRepository(IMongoCollection messageCollection) + { + _messageCollection = messageCollection + ?? throw new ArgumentNullException(nameof(messageCollection)); + } + + public IQueryable GetAllMessages() + { + return _messageCollection.AsQueryable(); + } + + public Task GetMessageById(ObjectId messageId) + { + return _messageCollection.AsQueryable().FirstOrDefaultAsync(t => t.Id == messageId); + } + + public Task CreateMessageAsync(Message message, CancellationToken cancellationToken) + { + return _messageCollection.InsertOneAsync(message, new InsertOneOptions(), cancellationToken); + } + } +} diff --git a/oxford_usergroup_2019/4_dataloader/MessageType.cs b/oxford_usergroup_2019/4_dataloader/MessageType.cs new file mode 100644 index 0000000..9af3531 --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/MessageType.cs @@ -0,0 +1,39 @@ +using HotChocolate.Resolvers; +using HotChocolate.Types; +using GreenDonut; +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class MessageType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Text).Type>(); + descriptor.Field("createdBy").Type>().Resolver(ctx => + { + UserRepository repository = ctx.Service(); + + IDataLoader dataLoader = ctx.BatchDataLoader( + "UserById", + repository.GetUsersAsync); + + return dataLoader.LoadAsync(ctx.Parent().UserId); + }); + descriptor.Field("replyTo").Type().Resolver(async ctx => + { + ObjectId? replyToId = ctx.Parent().ReplyToId; + if (replyToId.HasValue) + { + MessageRepository repository = ctx.Service(); + return await repository.GetMessageById(replyToId.Value); + } + return null; + }); + descriptor.Ignore(t => t.UserId); + descriptor.Ignore(t => t.ReplyToId); + } + } +} diff --git a/oxford_usergroup_2019/4_dataloader/Mutation.cs b/oxford_usergroup_2019/4_dataloader/Mutation.cs new file mode 100644 index 0000000..2aae183 --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/Mutation.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace HotChocolate.Examples.Paging +{ + public class Mutation + { + public async Task CreateMessageAsync( + MessageInput messageInput, + [Service]MessageRepository repository, + CancellationToken cancellationToken) + { + var message = new Message + { + Text = messageInput.Text, + UserId = messageInput.UserId, + ReplyToId = messageInput.ReplyToId, + Created = DateTimeOffset.UtcNow, + }; + + await repository.CreateMessageAsync(message, cancellationToken); + + return message; + } + + public async Task CreateUserAsync( + UserInput userInput, + [Service]UserRepository repository, + CancellationToken cancellationToken) + { + var user = new User + { + Name = userInput.Name, + Country = userInput.Country + }; + + await repository.CreateUserAsync(user, cancellationToken); + + return user; + } + } +} diff --git a/oxford_usergroup_2019/4_dataloader/MutationType.cs b/oxford_usergroup_2019/4_dataloader/MutationType.cs new file mode 100644 index 0000000..18bfcef --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/MutationType.cs @@ -0,0 +1,19 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class MutationType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.CreateMessageAsync(default, default, default)) + .Argument("messageInput", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.CreateUserAsync(default, default, default)) + .Argument("userInput", a => a.Type>()) + .Type(); + } + } +} diff --git a/oxford_usergroup_2019/4_dataloader/Program.cs b/oxford_usergroup_2019/4_dataloader/Program.cs new file mode 100644 index 0000000..3c1cee5 --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace HotChocolate.Examples.Paging +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/oxford_usergroup_2019/4_dataloader/QueryType.cs b/oxford_usergroup_2019/4_dataloader/QueryType.cs new file mode 100644 index 0000000..c883865 --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/QueryType.cs @@ -0,0 +1,35 @@ +using GreenDonut; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using HotChocolate.Types.Relay; + +namespace HotChocolate.Examples.Paging +{ + public class QueryType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field("messages") + .Resolver(ctx => ctx.Service().GetAllMessages()) + .UsePaging() + .UseFiltering() + .Use(next => context => ); + + descriptor.Field("usersByCountry") + .Argument("country", a => a.Type>()) + .Type>>>() + .Resolver(ctx => + { + var userRepository = ctx.Service(); + + IDataLoader userDataLoader = + ctx.GroupDataLoader( + "usersByCountry", + userRepository.GetUsersByCountry); + + return userDataLoader.LoadAsync(ctx.Argument("country")); + }); + } + } +} diff --git a/oxford_usergroup_2019/4_dataloader/README.md b/oxford_usergroup_2019/4_dataloader/README.md new file mode 100644 index 0000000..760b464 --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/README.md @@ -0,0 +1,21 @@ +# DataLoader Example + +This example shows how DataLoader can be used with Hot Chocolate and uses _Mongo_ as database. + +The **Cache DataLoader** and the **Batch DataLoader** are used in `MessageType.cs`. + +The **GroupDataLoader** is used in `QueryType.cs`. + +We support more DataLoader scenarious with Hot Chocolate than are showcased with this example. The example is aimed to show the most common use-cases. + +## Setup Mongo + +Personally I used docker to host my mongo db for the example. If you have setup docker that just add the following line in your terminal emulator of choice: + +```bash +docker run --name mongo -p 27017:27017 -d mongo mongod +``` + +If you don't have docker or do not want to use it you can install mongo from here: [https://www.mongodb.com/download-center/community](https://www.mongodb.com/download-center/community). + +[Hot Chocolate Documentation](https://hotchocolate.io) diff --git a/oxford_usergroup_2019/4_dataloader/Startup.cs b/oxford_usergroup_2019/4_dataloader/Startup.cs new file mode 100644 index 0000000..d445da4 --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/Startup.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.AspNetCore; +using HotChocolate; +using HotChocolate.Execution.Configuration; +using MongoDB.Driver; +using HotChocolate.Utilities; +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + // setup type conversion for object id + TypeConversion.Default.Register(from => ObjectId.Parse(from)); + TypeConversion.Default.Register(from => from.ToString()); + + // setup the repositories + services.AddSingleton(new MongoClient("mongodb://127.0.0.1:27017")); + services.AddSingleton(s => s.GetRequiredService().GetDatabase("PagingDemo")); + services.AddSingleton>(s => s.GetRequiredService().GetCollection("messages")); + services.AddSingleton>(s => s.GetRequiredService().GetCollection("users")); + services.AddSingleton(); + services.AddSingleton(); + + // this enables you to use DataLoader in your resolvers. + services.AddDataLoaderRegistry(); + + // Add GraphQL Services + services.AddGraphQL(SchemaBuilder.New() + .AddQueryType() + .AddMutationType()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(); + app.UsePlayground(); + } + } +} diff --git a/oxford_usergroup_2019/4_dataloader/User.cs b/oxford_usergroup_2019/4_dataloader/User.cs new file mode 100644 index 0000000..155c78b --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/User.cs @@ -0,0 +1,11 @@ +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class User + { + public ObjectId Id { get; set; } + public string Name { get; set; } + public string Country { get; set; } + } +} diff --git a/oxford_usergroup_2019/4_dataloader/UserInput.cs b/oxford_usergroup_2019/4_dataloader/UserInput.cs new file mode 100644 index 0000000..2511647 --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/UserInput.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.Examples.Paging +{ + public class UserInput + { + public string Name { get; set; } + public string Country { get; set; } + } +} diff --git a/oxford_usergroup_2019/4_dataloader/UserInputType.cs b/oxford_usergroup_2019/4_dataloader/UserInputType.cs new file mode 100644 index 0000000..1e38cc0 --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/UserInputType.cs @@ -0,0 +1,14 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class UserInputType + : InputObjectType + { + protected override void Configure(IInputObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Name).Type>(); + descriptor.Field(t => t.Country).Type>(); + } + } +} diff --git a/oxford_usergroup_2019/4_dataloader/UserRepository.cs b/oxford_usergroup_2019/4_dataloader/UserRepository.cs new file mode 100644 index 0000000..90399ed --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/UserRepository.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace HotChocolate.Examples.Paging +{ + public class UserRepository + { + private readonly IMongoCollection _userCollection; + + public UserRepository(IMongoCollection userCollection) + { + _userCollection = userCollection + ?? throw new ArgumentNullException(nameof(userCollection)); + } + + public IQueryable GetAllUsers() + { + return _userCollection.AsQueryable(); + } + + public async Task> GetUsersByCountry( + IReadOnlyList countries, + CancellationToken cancellationToken) + { + var filters = new List>(); + + foreach (string country in countries) + { + filters.Add(Builders.Filter.Eq(u => u.Country, country)); + } + + List users = await _userCollection + .Find(Builders.Filter.Or(filters)) + .ToListAsync(cancellationToken); + + return users.ToLookup(t => t.Country); + } + + public Task GetUserAsync(ObjectId userId, CancellationToken cancellationToken) + { + return _userCollection.Find(c => c.Id == userId) + .FirstOrDefaultAsync(cancellationToken); + } + + public Task CreateUserAsync(User user, CancellationToken cancellationToken) + { + return _userCollection.InsertOneAsync(user, new InsertOneOptions(), cancellationToken); + } + + public async Task> GetUsersAsync( + IReadOnlyCollection userIds, + CancellationToken cancellationToken) + { + var filters = new List>(); + foreach (ObjectId userId in userIds) + { + filters.Add(Builders.Filter.Eq(u => u.Id, userId)); + } + + List users = await _userCollection + .Find(Builders.Filter.Or(filters)) + .ToListAsync(cancellationToken); + + return users.ToDictionary(t => t.Id); + } + } +} diff --git a/oxford_usergroup_2019/4_dataloader/UserType.cs b/oxford_usergroup_2019/4_dataloader/UserType.cs new file mode 100644 index 0000000..7af7a05 --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/UserType.cs @@ -0,0 +1,14 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class UserType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Name).Type>(); + } + } +} diff --git a/oxford_usergroup_2019/4_dataloader/copy.txt b/oxford_usergroup_2019/4_dataloader/copy.txt new file mode 100644 index 0000000..4248412 --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/copy.txt @@ -0,0 +1,23 @@ +MessageType + +descriptor.Field("createdBy").Type>().Resolver(ctx => +{ + UserRepository repository = ctx.Service(); + + IDataLoader dataLoader = ctx.BatchDataLoader( + "UserById", + repository.GetUsersAsync); + + return dataLoader.LoadAsync(ctx.Parent().UserId); +}); + + +IDataLoader dataLoader = ctx.CacheDataLoader( + "MessageById", + repository.GetMessageById); + +return await dataLoader.LoadAsync(ctx.Parent().ReplyToId.Value); + +.UsePaging() + +docker run --name mongo -p 27017:27017 -d mongo mongod \ No newline at end of file diff --git a/oxford_usergroup_2019/4_dataloader/global.json b/oxford_usergroup_2019/4_dataloader/global.json new file mode 100644 index 0000000..89b3b0f --- /dev/null +++ b/oxford_usergroup_2019/4_dataloader/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "2.2.402" + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/.vscode/launch.json b/oxford_usergroup_2019/5_persisted_queries/.vscode/launch.json new file mode 100644 index 0000000..d3f889e --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/StarWars/bin/Debug/netcoreapp3.0/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/oxford_usergroup_2019/5_persisted_queries/.vscode/tasks.json b/oxford_usergroup_2019/5_persisted_queries/.vscode/tasks.json new file mode 100644 index 0000000..383f2db --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/.vscode/tasks.json @@ -0,0 +1,25 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + "StarWars", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/.vscode/launch.json b/oxford_usergroup_2019/5_persisted_queries/StarWars/.vscode/launch.json new file mode 100644 index 0000000..401f807 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.1/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/.vscode/tasks.json b/oxford_usergroup_2019/5_persisted_queries/StarWars/.vscode/tasks.json new file mode 100644 index 0000000..68ae5ff --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet build", + "type": "shell", + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/AspNetCore.StarWars.csproj b/oxford_usergroup_2019/5_persisted_queries/StarWars/AspNetCore.StarWars.csproj new file mode 100644 index 0000000..841febc --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/AspNetCore.StarWars.csproj @@ -0,0 +1,32 @@ + + + + netcoreapp3.0 + false + 7.2 + true + $(NoWarn);1591 + + + + portable + true + + + + pdbonly + true + + + + + + + + + + + + + + diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Data/CharacterRepository.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Data/CharacterRepository.cs new file mode 100644 index 0000000..dd09134 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Data/CharacterRepository.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StarWars.Models; + +namespace StarWars.Data +{ + public class CharacterRepository + { + private Dictionary _characters; + private Dictionary _starships; + + public CharacterRepository() + { + _characters = CreateCharacters().ToDictionary(t => t.Id); + _starships = CreateStarships().ToDictionary(t => t.Id); + } + + public ICharacter GetHero(Episode episode) + { + if (episode == Episode.Empire) + { + return _characters["1000"]; + } + return _characters["2001"]; + } + + public ICharacter GetCharacter(string id) + { + if (_characters.TryGetValue(id, out ICharacter c)) + { + return c; + } + return null; + } + + public Human GetHuman(string id) + { + if (_characters.TryGetValue(id, out ICharacter c) + && c is Human h) + { + return h; + } + return null; + } + + public Droid GetDroid(string id) + { + if (_characters.TryGetValue(id, out ICharacter c) + && c is Droid d) + { + return d; + } + return null; + } + + public IEnumerable Search(string text) + { +#if ASPNETCLASSIC + IEnumerable filteredCharacters = _characters.Values + .Where(t => t.Name.Contains(text)); +#else + IEnumerable filteredCharacters = _characters.Values + .Where(t => t.Name.Contains(text, + StringComparison.OrdinalIgnoreCase)); +#endif + + foreach (ICharacter character in filteredCharacters) + { + yield return character; + } + +#if ASPNETCLASSIC + IEnumerable filteredStarships = _starships.Values + .Where(t => t.Name.Contains(text)); +#else + IEnumerable filteredStarships = _starships.Values + .Where(t => t.Name.Contains(text, + StringComparison.OrdinalIgnoreCase)); +#endif + + foreach (Starship starship in filteredStarships) + { + yield return starship; + } + } + + private static IEnumerable CreateCharacters() + { + yield return new Human + { + Id = "1000", + Name = "Luke Skywalker", + Friends = new[] { "1002", "1003", "2000", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Tatooine" + }; + + yield return new Human + { + Id = "1001", + Name = "Darth Vader", + Friends = new[] { "1004" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Tatooine" + }; + + yield return new Human + { + Id = "1002", + Name = "Han Solo", + Friends = new[] { "1000", "1003", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi } + }; + + yield return new Human + { + Id = "1003", + Name = "Leia Organa", + Friends = new[] { "1000", "1002", "2000", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Alderaan" + }; + + yield return new Human + { + Id = "1004", + Name = "Wilhuff Tarkin", + Friends = new[] { "1001" }, + AppearsIn = new[] { Episode.NewHope } + }; + + yield return new Droid + { + Id = "2000", + Name = "C-3PO", + Friends = new[] { "1000", "1002", "1003", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + PrimaryFunction = "Protocol" + }; + + yield return new Droid + { + Id = "2001", + Name = "R2-D2", + Friends = new[] { "1000", "1002", "1003" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + PrimaryFunction = "Astromech" + }; + } + + private static IEnumerable CreateStarships() + { + yield return new Starship + { + Id = "3000", + Name = "TIE Advanced x1", + Length = 9.2 + }; + } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Data/ReviewRepository.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Data/ReviewRepository.cs new file mode 100644 index 0000000..a586306 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Data/ReviewRepository.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using StarWars.Models; + +namespace StarWars.Data +{ + public class ReviewRepository + { + private readonly Dictionary> _data = + new Dictionary>(); + + public void AddReview(Episode episode, Review review) + { + if (!_data.TryGetValue(episode, out List reviews)) + { + reviews = new List(); + _data[episode] = reviews; + } + + reviews.Add(review); + } + + public IEnumerable GetReviews(Episode episode) + { + if (_data.TryGetValue(episode, out List reviews)) + { + return reviews; + } + return Array.Empty(); + } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Droid.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Droid.cs new file mode 100644 index 0000000..0ee4213 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Droid.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A droid in the Star Wars universe. + /// + public class Droid + : ICharacter + { + /// + public string Id { get; set; } + + /// + public string Name { get; set; } + + /// + public IReadOnlyList Friends { get; set; } + + /// + public IReadOnlyList AppearsIn { get; set; } + + /// + /// The droid's primary function. + /// + public string PrimaryFunction { get; set; } + + /// + public double Height { get; } = 1.72d; + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Episode.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Episode.cs new file mode 100644 index 0000000..6900cf6 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Episode.cs @@ -0,0 +1,21 @@ +namespace StarWars.Models +{ + /// + /// The Star Wars episodes. + /// + public enum Episode + { + /// + /// Star Wars Episode IV: A New Hope + /// + NewHope, + /// + /// Star Wars Episode V: Empire Strikes Back + /// + Empire, + /// + /// Star Wars Episode VI: Return of the Jedi + /// + Jedi + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Human.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Human.cs new file mode 100644 index 0000000..caf6021 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Human.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A human character in the Star Wars universe. + /// + public class Human + : ICharacter + { + /// + public string Id { get; set; } + + /// + public string Name { get; set; } + + /// + public IReadOnlyList Friends { get; set; } + + /// + public IReadOnlyList AppearsIn { get; set; } + + /// + /// The planet the character is originally from. + /// + public string HomePlanet { get; set; } + + /// + public double Height { get; } = 1.72d; + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/ICharacter.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/ICharacter.cs new file mode 100644 index 0000000..f9186c1 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/ICharacter.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A character in the Star Wars universe. + /// + public interface ICharacter + { + /// + /// The unique identifier for the character. + /// + string Id { get; } + + /// + /// The name of the character. + /// + string Name { get; } + + /// + /// The names of the character's friends. + /// + IReadOnlyList Friends { get; } + + /// + /// The episodes the character appears in. + /// + IReadOnlyList AppearsIn { get; } + + /// + /// The height of the character. + /// + double Height { get; } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Review.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Review.cs new file mode 100644 index 0000000..3f18f16 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Review.cs @@ -0,0 +1,18 @@ +namespace StarWars.Models +{ + /// + /// A review of a particular movie. + /// + public class Review + { + /// + /// The number of stars given for this review. + /// + public int Stars { get; set; } + + /// + /// An explanation for the rating. + /// + public string Commentary { get; set; } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Starship.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Starship.cs new file mode 100644 index 0000000..5d7c241 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Starship.cs @@ -0,0 +1,23 @@ +namespace StarWars.Models +{ + /// + /// A starship in the Star Wars universe. + /// + public class Starship + { + /// + /// The Id of the starship. + /// + public string Id { get; set; } + + /// + /// The name of the starship. + /// + public string Name { get; set; } + + /// + /// The length of the starship. + /// + public double Length { get; set; } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Unit.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Unit.cs new file mode 100644 index 0000000..15316aa --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Models/Unit.cs @@ -0,0 +1,11 @@ +namespace StarWars.Models +{ + /// + /// Different units of measurement. + /// + public enum Unit + { + Foot, + Meters + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Mutation.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Mutation.cs new file mode 100644 index 0000000..45d4adc --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Mutation.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Subscriptions; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Mutation + { + private readonly ReviewRepository _repository; + + public Mutation(ReviewRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Creates a review for a given Star Wars episode. + /// + /// The episode to review. + /// The review. + /// The event sending service. + /// The created review. + public async Task CreateReview( + Episode episode, Review review, + [Service]IEventSender eventSender) + { + _repository.AddReview(episode, review); + await eventSender.SendAsync(new OnReviewMessage(episode, review)); + return review; + } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/OnReviewMessage.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/OnReviewMessage.cs new file mode 100644 index 0000000..95f9cff --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/OnReviewMessage.cs @@ -0,0 +1,23 @@ +using HotChocolate.Language; +using HotChocolate.Subscriptions; +using StarWars.Models; + +namespace StarWars +{ + public class OnReviewMessage + : EventMessage + { + public OnReviewMessage(Episode episode, Review review) + : base(CreateEventDescription(episode), review) + { + } + + private static EventDescription CreateEventDescription(Episode episode) + { + return new EventDescription("onReview", + new ArgumentNode("episode", + new EnumValueNode( + episode.ToString().ToUpperInvariant()))); + } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Program.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Program.cs new file mode 100644 index 0000000..0358799 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace StarWars +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Query.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Query.cs new file mode 100644 index 0000000..887534e --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Query.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using HotChocolate.Resolvers; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Query + { + private readonly CharacterRepository _repository; + + public Query(CharacterRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Retrieve a hero by a particular Star Wars episode. + /// + /// The episode to look up by. + /// The character. + public ICharacter GetHero(Episode episode) + { + return _repository.GetHero(episode); + } + + /// + /// Gets a human by Id. + /// + /// The Id of the human to retrieve. + /// The human. + public Human GetHuman(string id) + { + return _repository.GetHuman(id); + } + + /// + /// Get a particular droid by Id. + /// + /// The Id of the droid. + /// The droid. + public Droid GetDroid(string id) + { + return _repository.GetDroid(id); + } + + public IEnumerable GetCharacter(string[] characterIds, IResolverContext context) + { + foreach (string characterId in characterIds) + { + ICharacter character = _repository.GetCharacter(characterId); + if (character == null) + { + context.ReportError( + "Could not resolve a charachter for the " + + $"character-id {characterId}."); + } + else + { + yield return character; + } + } + } + + public IEnumerable Search(string text) + { + return _repository.Search(text); + } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Resolvers/SharedResolvers.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Resolvers/SharedResolvers.cs new file mode 100644 index 0000000..3911604 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Resolvers/SharedResolvers.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using HotChocolate; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars.Resolvers +{ + public class SharedResolvers + { + public IEnumerable GetCharacter( + [Parent]ICharacter character, + [Service]CharacterRepository repository) + { + foreach (string friendId in character.Friends) + { + ICharacter friend = repository.GetCharacter(friendId); + if (friend != null) + { + yield return friend; + } + } + } + + public double GetHeight(Unit? unit, [Parent]ICharacter character) + => ConvertToUnit(character.Height, unit); + + public double GetLength(Unit? unit, [Parent]Starship starship) + => ConvertToUnit(starship.Length, unit); + + private double ConvertToUnit(double length, Unit? unit) + { + if (unit == Unit.Foot) + { + return length * 3.28084d; + } + return length; + } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Startup.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Startup.cs new file mode 100644 index 0000000..2bab6bb --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Startup.cs @@ -0,0 +1,50 @@ +using HotChocolate; +using HotChocolate.AspNetCore; +using HotChocolate.Execution; +using HotChocolate.Subscriptions; +using HotChocolate.PersistedQueries.FileSystem; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using StarWars.Data; +using StarWars.Types; + +namespace StarWars +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + // Add the custom services like repositories etc ... + services.AddSingleton(); + services.AddSingleton(); + + // Add in-memory event provider + services.AddInMemorySubscriptionProvider(); + + services.AddReadOnlyFileSystemQueryStorage("./queries"); + + // Add GraphQL Services + services.AddGraphQL(sp => SchemaBuilder.New() + .AddServices(sp) + .AddQueryType() + .AddMutationType() + .AddSubscriptionType() + .AddType() + .AddType() + .AddType() + .Create(), + builder => builder.UsePersistedQueryPipeline()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app + .UseWebSockets() + .UseGraphQL("/graphql"); + } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Subscription.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Subscription.cs new file mode 100644 index 0000000..06a88b2 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Subscription.cs @@ -0,0 +1,23 @@ +using System; +using HotChocolate.Subscriptions; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Subscription + { + private readonly ReviewRepository _repository; + + public Subscription(ReviewRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + public Review OnReview(Episode episode, IEventMessage message) + { + return (Review)message.Payload; + } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/CharacterType.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/CharacterType.cs new file mode 100644 index 0000000..b9c5c98 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/CharacterType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; + +namespace StarWars.Types +{ + public class CharacterType + : InterfaceType + { + protected override void Configure(IInterfaceTypeDescriptor descriptor) + { + descriptor.Name("Character"); + + descriptor.Field(f => f.Id) + .Type>(); + + descriptor.Field(f => f.Name) + .Type(); + + descriptor.Field(f => f.Friends) + .UsePaging(); + + descriptor.Field(f => f.AppearsIn) + .Type>(); + + descriptor.Field(f => f.Height) + .Type() + .Argument("unit", a => a.Type>()); + } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/DroidType.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/DroidType.cs new file mode 100644 index 0000000..c34fe9a --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/DroidType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class DroidType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.AppearsIn) + .Type>(); + + descriptor.Field(r => r.GetCharacter(default, default)) + .UsePaging() + .Name("friends"); + + descriptor.Field(t => t.GetHeight(default, default)) + .Type() + .Argument("unit", a => a.Type>()) + .Name("height"); + } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/EpisodeType.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/EpisodeType.cs new file mode 100644 index 0000000..bfb484d --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/EpisodeType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class EpisodeType + : EnumType + { + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/HumanType.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/HumanType.cs new file mode 100644 index 0000000..3ab1c6a --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/HumanType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class HumanType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.AppearsIn) + .Type>(); + + descriptor.Field(r => r.GetCharacter(default, default)) + .UsePaging() + .Name("friends"); + + descriptor.Field(t => t.GetHeight(default, default)) + .Type() + .Argument("unit", a => a.Type>()) + .Name("height"); + } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/MutationType.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/MutationType.cs new file mode 100644 index 0000000..71963b7 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/MutationType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Types; + +namespace StarWars.Types +{ + public class MutationType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.CreateReview(default, default, default)) + .Type>() + .Argument("episode", a => a.Type>()) + .Argument("review", a => a.Type>()); + } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/QueryType.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/QueryType.cs new file mode 100644 index 0000000..193cf89 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/QueryType.cs @@ -0,0 +1,22 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class QueryType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetHero(default)) + .Type() + .Argument("episode", a => a.DefaultValue(Episode.NewHope)); + + descriptor.Field(t => t.GetCharacter(default, default)) + .Type>>>(); + + descriptor.Field(t => t.Search(default)) + .Type>(); + } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/ReviewInputType.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/ReviewInputType.cs new file mode 100644 index 0000000..300fbe6 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/ReviewInputType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class ReviewInputType + : InputObjectType + { + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/ReviewType.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/ReviewType.cs new file mode 100644 index 0000000..be94b38 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/ReviewType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class ReviewType + : ObjectType + { + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/SearchResultType.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/SearchResultType.cs new file mode 100644 index 0000000..7d1bb88 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/SearchResultType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class SearchResultType + : UnionType + { + protected override void Configure(IUnionTypeDescriptor descriptor) + { + descriptor.Name("SearchResult"); + descriptor.Type>(); + descriptor.Type(); + descriptor.Type(); + } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/StarshipType.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/StarshipType.cs new file mode 100644 index 0000000..7ad6b10 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/StarshipType.cs @@ -0,0 +1,18 @@ +using HotChocolate.Types; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class StarshipType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.GetLength(default, default)); + } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/SubscriptionType.cs b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/SubscriptionType.cs new file mode 100644 index 0000000..ed872e5 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/Types/SubscriptionType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace StarWars.Types +{ + public class SubscriptionType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.OnReview(default, default)) + .Type>() + .Argument("episode", arg => arg.Type>()); + } + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/StarWars/queries/foo b/oxford_usergroup_2019/5_persisted_queries/StarWars/queries/foo new file mode 100644 index 0000000..985c3eb --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/StarWars/queries/foo @@ -0,0 +1,5 @@ +query foo { + hero(episode: NEWHOPE) { + name + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/copy.txt b/oxford_usergroup_2019/5_persisted_queries/copy.txt new file mode 100644 index 0000000..1e02c6a --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/copy.txt @@ -0,0 +1,6 @@ + + +services.AddReadOnlyFileSystemQueryStorage("./queries"); + +, + b => b.UsePersistedQueryPipeline()) diff --git a/oxford_usergroup_2019/5_persisted_queries/global.json b/oxford_usergroup_2019/5_persisted_queries/global.json new file mode 100644 index 0000000..79422f0 --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "3.0.100" + } +} diff --git a/oxford_usergroup_2019/5_persisted_queries/queries/foo b/oxford_usergroup_2019/5_persisted_queries/queries/foo new file mode 100644 index 0000000..985c3eb --- /dev/null +++ b/oxford_usergroup_2019/5_persisted_queries/queries/foo @@ -0,0 +1,5 @@ +query foo { + hero(episode: NEWHOPE) { + name + } +} diff --git a/oxford_usergroup_2019/6_client/.config/dotnet-tools.json b/oxford_usergroup_2019/6_client/.config/dotnet-tools.json new file mode 100644 index 0000000..e295edd --- /dev/null +++ b/oxford_usergroup_2019/6_client/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "strawberryshake.tools": { + "version": "11.0.0-preview.52", + "commands": [ + "dotnet-graphql" + ] + } + } +} \ No newline at end of file diff --git a/oxford_usergroup_2019/6_client/.vscode/launch.json b/oxford_usergroup_2019/6_client/.vscode/launch.json new file mode 100644 index 0000000..e8d3eba --- /dev/null +++ b/oxford_usergroup_2019/6_client/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp3.0/Console.dll", + "args": [], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/oxford_usergroup_2019/6_client/.vscode/tasks.json b/oxford_usergroup_2019/6_client/.vscode/tasks.json new file mode 100644 index 0000000..31c32bd --- /dev/null +++ b/oxford_usergroup_2019/6_client/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/oxford_usergroup_2019/6_client/Console.csproj b/oxford_usergroup_2019/6_client/Console.csproj new file mode 100644 index 0000000..ccd6b4e --- /dev/null +++ b/oxford_usergroup_2019/6_client/Console.csproj @@ -0,0 +1,18 @@ + + + + Exe + netcoreapp3.0 + StarWarsClientDemo + 8.0 + enable + true + + + + + + + + + diff --git a/oxford_usergroup_2019/6_client/Program.cs b/oxford_usergroup_2019/6_client/Program.cs new file mode 100644 index 0000000..d5c884e --- /dev/null +++ b/oxford_usergroup_2019/6_client/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace StarWarsClientDemo +{ + class Program + { + static async Task Main(string[] args) + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddHttpClient( + "StarWarsClient", + c => c.BaseAddress = new Uri("http://localhost:5000/graphql")); + serviceCollection.AddStarWarsClient(); + + IServiceProvider services = serviceCollection.BuildServiceProvider(); + var client = services.GetRequiredService(); + var result = await client.GetHeroAsync(Episode.NewHope); + result.EnsureNoErrors(); + Console.WriteLine(result.Data?.Hero?.Name); + } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/.hash.md5 b/oxford_usergroup_2019/6_client/StarWars/Generated/.hash.md5 new file mode 100644 index 0000000..f17a9b8 --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/.hash.md5 @@ -0,0 +1 @@ +11.0.0.0__LSeanaYdQxCIK+xz7yj09A==_oUYS/GSD9Hy7DsT1xV41hA==_w3yAYkv62RXkLg0vrwAo/A== \ No newline at end of file diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/Character.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/Character.cs new file mode 100644 index 0000000..9342faa --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/Character.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public class Character + : ICharacter + { + public Character( + string? name, + ICharacterConnection? friends) + { + Name = name; + Friends = friends; + } + + public string? Name { get; } + + public ICharacterConnection? Friends { get; } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/CharacterConnection.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/CharacterConnection.cs new file mode 100644 index 0000000..befcf00 --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/CharacterConnection.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public class CharacterConnection + : ICharacterConnection + { + public CharacterConnection( + IReadOnlyList? nodes) + { + Nodes = nodes; + } + + public IReadOnlyList? Nodes { get; } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/Episode.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/Episode.cs new file mode 100644 index 0000000..3133846 --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/Episode.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public enum Episode + { + NewHope, + Empire, + Jedi + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/EpisodeValueSerializer.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/EpisodeValueSerializer.cs new file mode 100644 index 0000000..901fbcf --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/EpisodeValueSerializer.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public class EpisodeValueSerializer + : IValueSerializer + { + public string Name => "Episode"; + + public ValueKind Kind => ValueKind.Enum; + + public Type ClrType => typeof(Episode); + + public Type SerializationType => typeof(string); + + public object? Serialize(object? value) + { + if(value is null) + { + return null; + } + + var enumValue = (Episode)value; + + switch(enumValue) + { + case Episode.NewHope: + return "NEWHOPE"; + case Episode.Empire: + return "EMPIRE"; + case Episode.Jedi: + return "JEDI"; + default: + throw new NotSupportedException(); + } + } + + public object? Deserialize(object? serialized) + { + if(serialized is null) + { + return null; + } + + var stringValue = (string)serialized; + + switch(stringValue) + { + case "NEWHOPE": + return Episode.NewHope; + case "EMPIRE": + return Episode.Empire; + case "JEDI": + return Episode.Jedi; + default: + throw new NotSupportedException(); + } + } + + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/GetHero.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/GetHero.cs new file mode 100644 index 0000000..394f06f --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/GetHero.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public class GetHero + : IGetHero + { + public GetHero( + ICharacter? hero) + { + Hero = hero; + } + + public ICharacter? Hero { get; } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/GetHeroOperation.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/GetHeroOperation.cs new file mode 100644 index 0000000..2838856 --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/GetHeroOperation.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public class GetHeroOperation + : IOperation + { + public string Name => "GetHero"; + + public IDocument Document => Queries.Default; + + public Type ResultType => typeof(IGetHero); + + public Optional Episode { get; set; } + + public IReadOnlyList GetVariableValues() + { + var variables = new List(); + + if(Episode.HasValue) + { + variables.Add(new VariableValue("episode", "Episode", Episode.Value)); + } + + return variables; + } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/GetHeroResultParser.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/GetHeroResultParser.cs new file mode 100644 index 0000000..f6db5ff --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/GetHeroResultParser.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text.Json; +using StrawberryShake; +using StrawberryShake.Http; + +namespace StarWarsClientDemo +{ + public class GetHeroResultParser + : JsonResultParserBase + { + private readonly IValueSerializer _stringSerializer; + + public GetHeroResultParser(IValueSerializerResolver serializerResolver) + { + if(serializerResolver is null) + { + throw new ArgumentNullException(nameof(serializerResolver)); + } + _stringSerializer = serializerResolver.GetValueSerializer("String"); + } + + protected override IGetHero ParserData(JsonElement data) + { + return new GetHero + ( + ParseGetHeroHero(data, "hero") + ); + + } + + private ICharacter? ParseGetHeroHero( + JsonElement parent, + string field) + { + if (!parent.TryGetProperty(field, out JsonElement obj)) + { + return null; + } + + if (obj.ValueKind == JsonValueKind.Null) + { + return null; + } + + return new Character + ( + DeserializeNullableString(obj, "name"), + ParseGetHeroHeroFriends(obj, "friends") + ); + } + + private ICharacterConnection? ParseGetHeroHeroFriends( + JsonElement parent, + string field) + { + if (!parent.TryGetProperty(field, out JsonElement obj)) + { + return null; + } + + if (obj.ValueKind == JsonValueKind.Null) + { + return null; + } + + return new CharacterConnection + ( + ParseGetHeroHeroFriendsNodes(obj, "nodes") + ); + } + + private IReadOnlyList? ParseGetHeroHeroFriendsNodes( + JsonElement parent, + string field) + { + if (!parent.TryGetProperty(field, out JsonElement obj)) + { + return null; + } + + if (obj.ValueKind == JsonValueKind.Null) + { + return null; + } + + int objLength = obj.GetArrayLength(); + var list = new IHasName[objLength]; + for (int objIndex = 0; objIndex < objLength; objIndex++) + { + JsonElement element = obj[objIndex]; + list[objIndex] = new HasName + ( + DeserializeNullableString(element, "name") + ); + + } + + return list; + } + + private string? DeserializeNullableString(JsonElement obj, string fieldName) + { + if (!obj.TryGetProperty(fieldName, out JsonElement value)) + { + return null; + } + + if (value.ValueKind == JsonValueKind.Null) + { + return null; + } + + return (string?)_stringSerializer.Deserialize(value.GetString())!; + } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/HasName.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/HasName.cs new file mode 100644 index 0000000..33d5d56 --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/HasName.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public class HasName + : IHasName + { + public HasName( + string? name) + { + Name = name; + } + + public string? Name { get; } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/ICharacter.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/ICharacter.cs new file mode 100644 index 0000000..2d7206f --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/ICharacter.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public interface ICharacter + : IHasName + , IHasFriends + { + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/ICharacterConnection.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/ICharacterConnection.cs new file mode 100644 index 0000000..e7d900e --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/ICharacterConnection.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public interface ICharacterConnection + { + IReadOnlyList? Nodes { get; } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/IGetHero.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/IGetHero.cs new file mode 100644 index 0000000..ee6d1f3 --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/IGetHero.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public interface IGetHero + { + ICharacter? Hero { get; } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/IHasFriends.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/IHasFriends.cs new file mode 100644 index 0000000..aad276c --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/IHasFriends.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public interface IHasFriends + { + ICharacterConnection? Friends { get; } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/IHasName.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/IHasName.cs new file mode 100644 index 0000000..1b21d0e --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/IHasName.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public interface IHasName + { + string? Name { get; } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/IOnNewReview.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/IOnNewReview.cs new file mode 100644 index 0000000..92ea237 --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/IOnNewReview.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public interface IOnNewReview + { + IReview OnReview { get; } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/IReview.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/IReview.cs new file mode 100644 index 0000000..0744e01 --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/IReview.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public interface IReview + { + int Stars { get; } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/IStarWarsClient.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/IStarWarsClient.cs new file mode 100644 index 0000000..a13cfd4 --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/IStarWarsClient.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public interface IStarWarsClient + { + Task> GetHeroAsync( + Optional episode = default, + CancellationToken cancellationToken = default); + + Task> GetHeroAsync( + GetHeroOperation operation, + CancellationToken cancellationToken = default); + + Task> OnNewReviewAsync( + Optional episode = default, + CancellationToken cancellationToken = default); + + Task> OnNewReviewAsync( + OnNewReviewOperation operation, + CancellationToken cancellationToken = default); + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/OnNewReview.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/OnNewReview.cs new file mode 100644 index 0000000..99506e9 --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/OnNewReview.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public class OnNewReview + : IOnNewReview + { + public OnNewReview( + IReview onReview) + { + OnReview = onReview; + } + + public IReview OnReview { get; } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/OnNewReviewOperation.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/OnNewReviewOperation.cs new file mode 100644 index 0000000..8b8181c --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/OnNewReviewOperation.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public class OnNewReviewOperation + : IOperation + { + public string Name => "OnNewReview"; + + public IDocument Document => Queries.Default; + + public Type ResultType => typeof(IOnNewReview); + + public Optional Episode { get; set; } + + public IReadOnlyList GetVariableValues() + { + var variables = new List(); + + if(Episode.HasValue) + { + variables.Add(new VariableValue("episode", "Episode", Episode.Value)); + } + + return variables; + } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/OnNewReviewResultParser.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/OnNewReviewResultParser.cs new file mode 100644 index 0000000..0924065 --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/OnNewReviewResultParser.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text.Json; +using StrawberryShake; +using StrawberryShake.Http; + +namespace StarWarsClientDemo +{ + public class OnNewReviewResultParser + : JsonResultParserBase + { + private readonly IValueSerializer _intSerializer; + + public OnNewReviewResultParser(IValueSerializerResolver serializerResolver) + { + if(serializerResolver is null) + { + throw new ArgumentNullException(nameof(serializerResolver)); + } + _intSerializer = serializerResolver.GetValueSerializer("Int"); + } + + protected override IOnNewReview ParserData(JsonElement data) + { + return new OnNewReview + ( + ParseOnNewReviewOnReview(data, "onReview") + ); + + } + + private IReview ParseOnNewReviewOnReview( + JsonElement parent, + string field) + { + JsonElement obj = parent.GetProperty(field); + + return new Review + ( + DeserializeInt(obj, "stars") + ); + } + + private int DeserializeInt(JsonElement obj, string fieldName) + { + JsonElement value = obj.GetProperty(fieldName); + return (int)_intSerializer.Deserialize(value.GetInt32())!; + } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/Queries.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/Queries.cs new file mode 100644 index 0000000..f3ed772 --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/Queries.cs @@ -0,0 +1,481 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public class Queries + : IDocument + { + private readonly byte[] _hashName = new byte[] + { + 109, + 100, + 53, + 72, + 97, + 115, + 104 + }; + private readonly byte[] _hash = new byte[] + { + 97, + 80, + 97, + 101, + 72, + 115, + 116, + 72, + 109, + 109, + 79, + 87, + 99, + 119, + 98, + 112, + 102, + 67, + 79, + 97, + 79, + 119, + 61, + 61 + }; + private readonly byte[] _content = new byte[] + { + 113, + 117, + 101, + 114, + 121, + 32, + 71, + 101, + 116, + 72, + 101, + 114, + 111, + 40, + 36, + 101, + 112, + 105, + 115, + 111, + 100, + 101, + 58, + 32, + 69, + 112, + 105, + 115, + 111, + 100, + 101, + 32, + 61, + 32, + 78, + 69, + 87, + 72, + 79, + 80, + 69, + 41, + 32, + 123, + 32, + 95, + 95, + 116, + 121, + 112, + 101, + 110, + 97, + 109, + 101, + 32, + 104, + 101, + 114, + 111, + 40, + 101, + 112, + 105, + 115, + 111, + 100, + 101, + 58, + 32, + 36, + 101, + 112, + 105, + 115, + 111, + 100, + 101, + 41, + 32, + 123, + 32, + 95, + 95, + 116, + 121, + 112, + 101, + 110, + 97, + 109, + 101, + 32, + 46, + 46, + 46, + 32, + 72, + 97, + 115, + 78, + 97, + 109, + 101, + 32, + 46, + 46, + 46, + 32, + 72, + 97, + 115, + 70, + 114, + 105, + 101, + 110, + 100, + 115, + 32, + 125, + 32, + 125, + 32, + 115, + 117, + 98, + 115, + 99, + 114, + 105, + 112, + 116, + 105, + 111, + 110, + 32, + 79, + 110, + 78, + 101, + 119, + 82, + 101, + 118, + 105, + 101, + 119, + 40, + 36, + 101, + 112, + 105, + 115, + 111, + 100, + 101, + 58, + 32, + 69, + 112, + 105, + 115, + 111, + 100, + 101, + 32, + 61, + 32, + 78, + 69, + 87, + 72, + 79, + 80, + 69, + 41, + 32, + 123, + 32, + 95, + 95, + 116, + 121, + 112, + 101, + 110, + 97, + 109, + 101, + 32, + 111, + 110, + 82, + 101, + 118, + 105, + 101, + 119, + 40, + 101, + 112, + 105, + 115, + 111, + 100, + 101, + 58, + 32, + 36, + 101, + 112, + 105, + 115, + 111, + 100, + 101, + 41, + 32, + 123, + 32, + 95, + 95, + 116, + 121, + 112, + 101, + 110, + 97, + 109, + 101, + 32, + 115, + 116, + 97, + 114, + 115, + 32, + 125, + 32, + 125, + 32, + 102, + 114, + 97, + 103, + 109, + 101, + 110, + 116, + 32, + 72, + 97, + 115, + 78, + 97, + 109, + 101, + 32, + 111, + 110, + 32, + 67, + 104, + 97, + 114, + 97, + 99, + 116, + 101, + 114, + 32, + 123, + 32, + 95, + 95, + 116, + 121, + 112, + 101, + 110, + 97, + 109, + 101, + 32, + 110, + 97, + 109, + 101, + 32, + 125, + 32, + 102, + 114, + 97, + 103, + 109, + 101, + 110, + 116, + 32, + 72, + 97, + 115, + 70, + 114, + 105, + 101, + 110, + 100, + 115, + 32, + 111, + 110, + 32, + 67, + 104, + 97, + 114, + 97, + 99, + 116, + 101, + 114, + 32, + 123, + 32, + 95, + 95, + 116, + 121, + 112, + 101, + 110, + 97, + 109, + 101, + 32, + 102, + 114, + 105, + 101, + 110, + 100, + 115, + 32, + 123, + 32, + 95, + 95, + 116, + 121, + 112, + 101, + 110, + 97, + 109, + 101, + 32, + 110, + 111, + 100, + 101, + 115, + 32, + 123, + 32, + 95, + 95, + 116, + 121, + 112, + 101, + 110, + 97, + 109, + 101, + 32, + 46, + 46, + 46, + 32, + 72, + 97, + 115, + 78, + 97, + 109, + 101, + 32, + 125, + 32, + 125, + 32, + 125 + }; + + public ReadOnlySpan HashName => _hashName; + + public ReadOnlySpan Hash => _hash; + + public ReadOnlySpan Content => _content; + + public static Queries Default { get; } = new Queries(); + + public override string ToString() => + @"query GetHero($episode: Episode = NEWHOPE) { + hero(episode: $episode) { + ... HasName + ... HasFriends + } + } + + subscription OnNewReview($episode: Episode = NEWHOPE) { + onReview(episode: $episode) { + stars + } + } + + fragment HasName on Character { + name + } + + fragment HasFriends on Character { + friends { + nodes { + ... HasName + } + } + }"; + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/Review.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/Review.cs new file mode 100644 index 0000000..e188801 --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/Review.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public class Review + : IReview + { + public Review( + int stars) + { + Stars = stars; + } + + public int Stars { get; } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/StarWarsClient.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/StarWarsClient.cs new file mode 100644 index 0000000..388e31c --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/StarWarsClient.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using StrawberryShake; + +namespace StarWarsClientDemo +{ + public class StarWarsClient + : IStarWarsClient + { + private readonly IOperationExecutor _executor; + private readonly IOperationStreamExecutor _streamExecutor; + + public StarWarsClient(IOperationExecutor executor, IOperationStreamExecutor streamExecutor) + { + _executor = executor ?? throw new ArgumentNullException(nameof(executor)); + _streamExecutor = streamExecutor ?? throw new ArgumentNullException(nameof(streamExecutor)); + } + + public Task> GetHeroAsync( + Optional episode = default, + CancellationToken cancellationToken = default) + { + + return _executor.ExecuteAsync( + new GetHeroOperation { Episode = episode }, + cancellationToken); + } + + public Task> GetHeroAsync( + GetHeroOperation operation, + CancellationToken cancellationToken = default) + { + if(operation is null) + { + throw new ArgumentNullException(nameof(operation)); + } + + return _executor.ExecuteAsync(operation, cancellationToken); + } + + public Task> OnNewReviewAsync( + Optional episode = default, + CancellationToken cancellationToken = default) + { + + return _streamExecutor.ExecuteAsync( + new OnNewReviewOperation { Episode = episode }, + cancellationToken); + } + + public Task> OnNewReviewAsync( + OnNewReviewOperation operation, + CancellationToken cancellationToken = default) + { + if(operation is null) + { + throw new ArgumentNullException(nameof(operation)); + } + + return _streamExecutor.ExecuteAsync(operation, cancellationToken); + } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Generated/StarWarsClientServiceCollectionExtensions.cs b/oxford_usergroup_2019/6_client/StarWars/Generated/StarWarsClientServiceCollectionExtensions.cs new file mode 100644 index 0000000..a7f6d56 --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Generated/StarWarsClientServiceCollectionExtensions.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using StrawberryShake; +using StrawberryShake.Http; +using StrawberryShake.Http.Pipelines; +using StrawberryShake.Serializers; + +namespace StarWarsClientDemo +{ + public static class StarWarsClientServiceCollectionExtensions + { + public static IServiceCollection AddStarWarsClient( + this IServiceCollection serviceCollection) + { + if (serviceCollection is null) + { + throw new ArgumentNullException(nameof(serviceCollection)); + } + + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(sp => + HttpOperationExecutorBuilder.New() + .AddServices(sp) + .SetClient(ClientFactory) + .SetPipeline(PipelineFactory) + .Build()); + + serviceCollection.AddDefaultScalarSerializers(); + serviceCollection.AddEnumSerializers(); + serviceCollection.AddResultParsers(); + + serviceCollection.TryAddDefaultOperationSerializer(); + serviceCollection.TryAddDefaultHttpPipeline(); + + return serviceCollection; + } + + public static IServiceCollection AddDefaultScalarSerializers( + this IServiceCollection serviceCollection) + { + if (serviceCollection is null) + { + throw new ArgumentNullException(nameof(serviceCollection)); + } + + serviceCollection.AddSingleton(); + + foreach (IValueSerializer serializer in ValueSerializers.All) + { + serviceCollection.AddSingleton(serializer); + } + + return serviceCollection; + } + + private static IServiceCollection AddEnumSerializers( + this IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton(); + return serviceCollection; + } + + + private static IServiceCollection AddResultParsers( + this IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + return serviceCollection; + } + + private static IServiceCollection TryAddDefaultOperationSerializer( + this IServiceCollection serviceCollection) + { + serviceCollection.TryAddSingleton(); + return serviceCollection; + } + + private static IServiceCollection TryAddDefaultHttpPipeline( + this IServiceCollection serviceCollection) + { + serviceCollection.TryAddSingleton( + sp => HttpPipelineBuilder.New() + .Use() + .Use() + .Use() + .Build(sp)); + return serviceCollection; + } + + private static Func ClientFactory(IServiceProvider services) + { + var clientFactory = services.GetRequiredService(); + return () => clientFactory.CreateClient("StarWarsClient"); + } + + private static OperationDelegate PipelineFactory(IServiceProvider services) + { + return services.GetRequiredService(); + } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/Queries.graphql b/oxford_usergroup_2019/6_client/StarWars/Queries.graphql new file mode 100644 index 0000000..c93299f --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/Queries.graphql @@ -0,0 +1,24 @@ +query GetHero($episode: Episode = NEWHOPE) { + hero(episode: $episode) { + ...HasName + ...HasFriends + } +} + +subscription OnNewReview($episode: Episode = NEWHOPE) { + onReview(episode: $episode) { + stars + } +} + +fragment HasName on Character { + name +} + +fragment HasFriends on Character { + friends { + nodes { + ...HasName + } + } +} diff --git a/oxford_usergroup_2019/6_client/StarWars/StarWars.Extensions.graphql b/oxford_usergroup_2019/6_client/StarWars/StarWars.Extensions.graphql new file mode 100644 index 0000000..d4e04b0 --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/StarWars.Extensions.graphql @@ -0,0 +1,3 @@ +extend enum Episode { + NEWHOPE @name(value: "NewHope") +} \ No newline at end of file diff --git a/oxford_usergroup_2019/6_client/StarWars/StarWars.graphql b/oxford_usergroup_2019/6_client/StarWars/StarWars.graphql new file mode 100644 index 0000000..3d43f76 --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/StarWars.graphql @@ -0,0 +1,144 @@ +schema { + query: Query + mutation: Mutation + subscription: Subscription +} + +type Query { + character(characterIds: [String]): [Character!]! + "Get a particular droid by Id." + droid("The Id of the droid." id: String): Droid + "Retrieve a hero by a particular Star Wars episode." + hero("The episode to look up by." episode: Episode! = NEWHOPE): Character + "Gets a human by Id." + human("The Id of the human to retrieve." id: String): Human + search(text: String): [SearchResult] +} + +type Mutation { + "Creates a review for a given Star Wars episode." + createReview("The episode to review." episode: Episode! "The review." review: ReviewInput!): Review! +} + +type Subscription { + onReview(episode: Episode!): Review! +} + +"A human character in the Star Wars universe." +type Human implements Character { + "The episodes the character appears in." + appearsIn: [Episode] + friends(after: String before: String first: PaginationAmount last: PaginationAmount): CharacterConnection + height(unit: Unit): Float + "The planet the character is originally from." + homePlanet: String + "The unique identifier for the character." + id: ID! + "The name of the character." + name: String +} + +"A droid in the Star Wars universe." +type Droid implements Character { + "The episodes the character appears in." + appearsIn: [Episode] + friends(after: String before: String first: PaginationAmount last: PaginationAmount): CharacterConnection + height(unit: Unit): Float + "The unique identifier for the character." + id: ID! + "The name of the character." + name: String + "The droid's primary function." + primaryFunction: String +} + +"The Star Wars episodes." +enum Episode { + "Star Wars Episode IV: A New Hope" + NEWHOPE + "Star Wars Episode V: Empire Strikes Back" + EMPIRE + "Star Wars Episode VI: Return of the Jedi" + JEDI +} + +"A character in the Star Wars universe." +interface Character { + "The episodes the character appears in." + appearsIn: [Episode] + "The names of the character's friends." + friends(after: String before: String first: PaginationAmount last: PaginationAmount): CharacterConnection + "The height of the character." + height(unit: Unit): Float + "The unique identifier for the character." + id: ID! + "The name of the character." + name: String +} + +union SearchResult = Starship | Human | Droid + +"A review of a particular movie." +type Review { + "An explanation for the rating." + commentary: String + "The number of stars given for this review." + stars: Int! +} + +"A review of a particular movie." +input ReviewInput { + "An explanation for the rating." + commentary: String + "The number of stars given for this review." + stars: Int! +} + +"Different units of measurement." +enum Unit { + FOOT + METERS +} + +"A connection to a list of items." +type CharacterConnection { + "A list of edges." + edges: [CharacterEdge!] + "A flattened list of the nodes." + nodes: [Character] + "Information to aid in pagination." + pageInfo: PageInfo! + totalCount: Int! +} + +"A starship in the Star Wars universe." +type Starship { + "The Id of the starship." + id: String + "The length of the starship." + length: Float! + "The name of the starship." + name: String +} + +"Information about pagination in a connection." +type PageInfo { + "When paginating forwards, the cursor to continue." + endCursor: String + "Indicates whether more edges exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more edges exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! + "When paginating backwards, the cursor to continue." + startCursor: String +} + +"An edge in a connection." +type CharacterEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Character +} + +directive @authorize("The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access to the annotated resource." roles: [String!]) repeatable on OBJECT | FIELD_DEFINITION \ No newline at end of file diff --git a/oxford_usergroup_2019/6_client/StarWars/berry.json b/oxford_usergroup_2019/6_client/StarWars/berry.json new file mode 100644 index 0000000..3c596ed --- /dev/null +++ b/oxford_usergroup_2019/6_client/StarWars/berry.json @@ -0,0 +1,11 @@ +{ + "Schemas": [ + { + "Name": "StarWars", + "Type": "http", + "File": "StarWars.graphql", + "Url": "http://localhost:5000/graphql" + } + ], + "ClientName": "StarWarsClient" +} diff --git a/oxford_usergroup_2019/6_client/copy.txt b/oxford_usergroup_2019/6_client/copy.txt new file mode 100644 index 0000000..6e3f102 --- /dev/null +++ b/oxford_usergroup_2019/6_client/copy.txt @@ -0,0 +1,35 @@ +query GetHero { + hero(episode: EMPIRE) { + ...HasName + ... HasFriends + } +} + +fragment HasName on Character { + name +} + +fragment HasFriends on Character { + friends { + items: nodes { + ...HasName + } + } +} + + +extend enum Episode { + NEWHOPE @name(value: "NewHope") +} + +var serviceCollection = new ServiceCollection(); + serviceCollection.AddHttpClient( + "StarWarsClient", + c => c.BaseAddress = new Uri("http://localhost:5000/graphql")); + serviceCollection.AddStarWarsClient(); + + IServiceProvider services = serviceCollection.BuildServiceProvider(); + var client = services.GetRequiredService(); + var result = await client.GetHeroAsync(Episode.NewHope); + result.EnsureNoErrors(); + Console.WriteLine(result.Data?.Hero?.Name); \ No newline at end of file diff --git a/oxford_usergroup_2019/7_stitching/.vscode/launch.json b/oxford_usergroup_2019/7_stitching/.vscode/launch.json new file mode 100644 index 0000000..d883fe4 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/Gateway/bin/Debug/netcoreapp2.1/stitched.dll", + "args": [], + "cwd": "${workspaceFolder}/Gateway", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/oxford_usergroup_2019/7_stitching/.vscode/tasks.json b/oxford_usergroup_2019/7_stitching/.vscode/tasks.json new file mode 100644 index 0000000..31c32bd --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/oxford_usergroup_2019/7_stitching/ContractSchema/ContractStorage.cs b/oxford_usergroup_2019/7_stitching/ContractSchema/ContractStorage.cs new file mode 100644 index 0000000..57295c7 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/ContractSchema/ContractStorage.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Demo.Contracts +{ + public class ContractStorage + { + public List Contracts { get; } = new List + { + new LifeInsuranceContract + { + Id = "1", + CustomerId= "1", + Premium = 123456.11 + }, + new LifeInsuranceContract + { + Id = "2", + CustomerId= "1", + Premium = 456789.12 + }, + new LifeInsuranceContract + { + Id = "3", + CustomerId = "2", + Premium = 789.12 + }, + new SomeOtherContract + { + Id = "1", + CustomerId= "1", + ExpiryDate = new DateTime(2015, 2, 1, 0,0,0, DateTimeKind.Utc) + }, + new SomeOtherContract + { + Id = "2", + CustomerId= "2", + ExpiryDate = new DateTime(2015, 5, 1, 0,0,0, DateTimeKind.Utc) + }, + new SomeOtherContract + { + Id = "3", + CustomerId= "3", + ExpiryDate = new DateTime(2017, 1, 30, 0,0,0, DateTimeKind.Utc) + }, + new SomeOtherContract + { + Id = "4", + CustomerId= "3", + ExpiryDate = new DateTime(2020, 1, 1, 0,0,0, DateTimeKind.Utc) + } + }; + } +} diff --git a/oxford_usergroup_2019/7_stitching/ContractSchema/ContractType.cs b/oxford_usergroup_2019/7_stitching/ContractSchema/ContractType.cs new file mode 100644 index 0000000..ff5b38b --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/ContractSchema/ContractType.cs @@ -0,0 +1,14 @@ +using HotChocolate.Types; + +namespace Demo.Contracts +{ + public class ContractType + : InterfaceType + { + protected override void Configure(IInterfaceTypeDescriptor descriptor) + { + descriptor.Name("Contract"); + descriptor.Field("id").Type>(); + } + } +} diff --git a/oxford_usergroup_2019/7_stitching/ContractSchema/IContract.cs b/oxford_usergroup_2019/7_stitching/ContractSchema/IContract.cs new file mode 100644 index 0000000..64397c4 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/ContractSchema/IContract.cs @@ -0,0 +1,9 @@ +namespace Demo.Contracts +{ + public interface IContract + { + string Id { get; } + + string CustomerId { get; } + } +} diff --git a/oxford_usergroup_2019/7_stitching/ContractSchema/LifeInsuranceContract.cs b/oxford_usergroup_2019/7_stitching/ContractSchema/LifeInsuranceContract.cs new file mode 100644 index 0000000..93a577a --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/ContractSchema/LifeInsuranceContract.cs @@ -0,0 +1,12 @@ +namespace Demo.Contracts +{ + public class LifeInsuranceContract + : IContract + { + public string Id { get; set; } + + public string CustomerId { get; set; } + + public double Premium { get; set; } + } +} diff --git a/oxford_usergroup_2019/7_stitching/ContractSchema/LifeInsuranceContractType.cs b/oxford_usergroup_2019/7_stitching/ContractSchema/LifeInsuranceContractType.cs new file mode 100644 index 0000000..c629ecf --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/ContractSchema/LifeInsuranceContractType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Types; + +namespace Demo.Contracts +{ + public class LifeInsuranceContractType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.CustomerId).Ignore(); + } + } +} diff --git a/oxford_usergroup_2019/7_stitching/ContractSchema/Program.cs b/oxford_usergroup_2019/7_stitching/ContractSchema/Program.cs new file mode 100644 index 0000000..4ec4019 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/ContractSchema/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Demo.Contracts +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseUrls("http://localhost:5051") + .UseStartup(); + } +} diff --git a/oxford_usergroup_2019/7_stitching/ContractSchema/Query.cs b/oxford_usergroup_2019/7_stitching/ContractSchema/Query.cs new file mode 100644 index 0000000..363b8f7 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/ContractSchema/Query.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HotChocolate.Types.Relay; + +namespace Demo.Contracts +{ + public class Query + { + private readonly IdSerializer _idSerializer = new IdSerializer(); + private readonly ContractStorage _contractStorage; + + public Query(ContractStorage contractStorage) + { + _contractStorage = contractStorage + ?? throw new ArgumentNullException(nameof(contractStorage)); + } + + public IContract GetContract(string contractId) + { + IdValue value = _idSerializer.Deserialize(contractId); + + if (value.TypeName == nameof(LifeInsuranceContract)) + { + return _contractStorage.Contracts + .OfType() + .FirstOrDefault(t => t.Id.Equals(value.Value)); + } + else + { + return _contractStorage.Contracts + .OfType() + .FirstOrDefault(t => t.Id.Equals(value.Value)); + } + } + + public IEnumerable GetContracts(string customerId) + { + IdValue value = _idSerializer.Deserialize(customerId); + + return _contractStorage.Contracts + .Where(t => t.CustomerId.Equals(value.Value)); + } + } +} diff --git a/oxford_usergroup_2019/7_stitching/ContractSchema/QueryType.cs b/oxford_usergroup_2019/7_stitching/ContractSchema/QueryType.cs new file mode 100644 index 0000000..c012283 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/ContractSchema/QueryType.cs @@ -0,0 +1,20 @@ +using HotChocolate.Types; + +namespace Demo.Contracts +{ + public class QueryType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetContract(default)) + .Argument("contractId", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.GetContracts(default)) + .Argument("customerId", a => a.Type>()) + .Type>>(); + } + } +} diff --git a/oxford_usergroup_2019/7_stitching/ContractSchema/SomeOtherContract.cs b/oxford_usergroup_2019/7_stitching/ContractSchema/SomeOtherContract.cs new file mode 100644 index 0000000..54c1572 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/ContractSchema/SomeOtherContract.cs @@ -0,0 +1,14 @@ +using System; + +namespace Demo.Contracts +{ + public class SomeOtherContract + : IContract + { + public string Id { get; set; } + + public string CustomerId { get; set; } + + public DateTime ExpiryDate { get; set; } + } +} diff --git a/oxford_usergroup_2019/7_stitching/ContractSchema/SomeOtherContractType.cs b/oxford_usergroup_2019/7_stitching/ContractSchema/SomeOtherContractType.cs new file mode 100644 index 0000000..303d676 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/ContractSchema/SomeOtherContractType.cs @@ -0,0 +1,23 @@ +using HotChocolate.Types; + +namespace Demo.Contracts +{ + public class SomeOtherContractType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.CustomerId) + .Ignore(); + + descriptor.Field(t => t.ExpiryDate) + .Type>(); + } + } +} diff --git a/oxford_usergroup_2019/7_stitching/ContractSchema/Startup.cs b/oxford_usergroup_2019/7_stitching/ContractSchema/Startup.cs new file mode 100644 index 0000000..71ea528 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/ContractSchema/Startup.cs @@ -0,0 +1,39 @@ +using HotChocolate; +using HotChocolate.AspNetCore; +using Demo.Contracts; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Demo.Contracts +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + + // Add GraphQL Services + services.AddGraphQL(Schema.Create(c => + { + c.RegisterQueryType(); + c.RegisterType(); + c.RegisterType(); + + c.UseGlobalObjectIdentifier(); + })); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(); + app.UsePlayground(); + } + } +} diff --git a/oxford_usergroup_2019/7_stitching/ContractSchema/contract.csproj b/oxford_usergroup_2019/7_stitching/ContractSchema/contract.csproj new file mode 100644 index 0000000..c36ea18 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/ContractSchema/contract.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp2.1 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + + + \ No newline at end of file diff --git a/oxford_usergroup_2019/7_stitching/CustomerSchema/Consultant.cs b/oxford_usergroup_2019/7_stitching/CustomerSchema/Consultant.cs new file mode 100644 index 0000000..903e6c5 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/CustomerSchema/Consultant.cs @@ -0,0 +1,9 @@ +namespace Demo.Customers +{ + public class Consultant + : ICustomerOrConsultant + { + public string Id { get; set; } + public string Name { get; set; } + } +} diff --git a/oxford_usergroup_2019/7_stitching/CustomerSchema/ConsultantType.cs b/oxford_usergroup_2019/7_stitching/CustomerSchema/ConsultantType.cs new file mode 100644 index 0000000..d3d3e94 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/CustomerSchema/ConsultantType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace Demo.Customers +{ + public class ConsultantType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Name).Type>(); + } + } +} diff --git a/oxford_usergroup_2019/7_stitching/CustomerSchema/Customer.cs b/oxford_usergroup_2019/7_stitching/CustomerSchema/Customer.cs new file mode 100644 index 0000000..a0c5abc --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/CustomerSchema/Customer.cs @@ -0,0 +1,10 @@ +namespace Demo.Customers +{ + public class Customer + : ICustomerOrConsultant + { + public string Id { get; set; } + public string Name { get; set; } + public string ConsultantId { get; set; } + } +} diff --git a/oxford_usergroup_2019/7_stitching/CustomerSchema/CustomerOrConsultantType.cs b/oxford_usergroup_2019/7_stitching/CustomerSchema/CustomerOrConsultantType.cs new file mode 100644 index 0000000..e6aed53 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/CustomerSchema/CustomerOrConsultantType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace Demo.Customers +{ + public class CustomerOrConsultantType + : UnionType + { + protected override void Configure(IUnionTypeDescriptor descriptor) + { + descriptor.Name("CustomerOrConsultant"); + descriptor.Type(); + descriptor.Type(); + } + } +} diff --git a/oxford_usergroup_2019/7_stitching/CustomerSchema/CustomerRepository.cs b/oxford_usergroup_2019/7_stitching/CustomerSchema/CustomerRepository.cs new file mode 100644 index 0000000..0b11ec7 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/CustomerSchema/CustomerRepository.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace Demo.Customers +{ + public class CustomerRepository + { + public List Customers { get; } = new List + { + new Customer + { + Id = "1", + Name = "Freddy Freeman", + ConsultantId = "1" + }, + new Customer + { + Id = "2", + Name = "Carol Danvers", + ConsultantId = "1" + }, + new Customer + { + Id = "3", + Name = "Walter Lawson", + ConsultantId = "2" + } + }; + + public List Consultants { get; } = new List + { + new Consultant + { + Id = "1", + Name = "Jordan Belfort", + }, + new Consultant + { + Id = "2", + Name = "Gordon Gekko", + } + }; + } +} diff --git a/oxford_usergroup_2019/7_stitching/CustomerSchema/CustomerResolver.cs b/oxford_usergroup_2019/7_stitching/CustomerSchema/CustomerResolver.cs new file mode 100644 index 0000000..f30784e --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/CustomerSchema/CustomerResolver.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq; +using HotChocolate; + +namespace Demo.Customers +{ + public class CustomerResolver + { + public Consultant GetConsultant( + Customer customer, + [Service]CustomerRepository repository) + { + if (customer.ConsultantId != null) + { + return repository.Consultants.FirstOrDefault( + t => t.Id.Equals(customer.ConsultantId)); + } + return null; + } + } +} diff --git a/oxford_usergroup_2019/7_stitching/CustomerSchema/CustomerType.cs b/oxford_usergroup_2019/7_stitching/CustomerSchema/CustomerType.cs new file mode 100644 index 0000000..79e2d17 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/CustomerSchema/CustomerType.cs @@ -0,0 +1,20 @@ +using HotChocolate.Types; + +namespace Demo.Customers +{ + public class CustomerType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Name).Type>(); + descriptor.Field(t => t.ConsultantId).Ignore(); + + descriptor.Field( + t => t.GetConsultant(default, default)) + .Type(); + } + } +} diff --git a/oxford_usergroup_2019/7_stitching/CustomerSchema/ICustomerOrConsultant.cs b/oxford_usergroup_2019/7_stitching/CustomerSchema/ICustomerOrConsultant.cs new file mode 100644 index 0000000..17abcb0 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/CustomerSchema/ICustomerOrConsultant.cs @@ -0,0 +1,7 @@ +namespace Demo.Customers +{ + public interface ICustomerOrConsultant + { + + } +} diff --git a/oxford_usergroup_2019/7_stitching/CustomerSchema/Program.cs b/oxford_usergroup_2019/7_stitching/CustomerSchema/Program.cs new file mode 100644 index 0000000..15df5b0 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/CustomerSchema/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Demo.Customers +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseUrls("http://localhost:5050") + .UseStartup(); + } +} diff --git a/oxford_usergroup_2019/7_stitching/CustomerSchema/Query.cs b/oxford_usergroup_2019/7_stitching/CustomerSchema/Query.cs new file mode 100644 index 0000000..f4a25e9 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/CustomerSchema/Query.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HotChocolate.Types.Relay; + +namespace Demo.Customers +{ + public class Query + { + private readonly IdSerializer _idSerializer = new IdSerializer(); + private readonly CustomerRepository _repository; + + public Query(CustomerRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + public Customer GetCustomer(string id) + { + IdValue value = _idSerializer.Deserialize(id); + return _repository.Customers + .FirstOrDefault(t => t.Id.Equals(value.Value)); + } + + public IEnumerable GetCustomers() + { + return _repository.Customers; + } + + public Consultant GetConsultant(string id) + { + IdValue value = _idSerializer.Deserialize(id); + return _repository.Consultants + .FirstOrDefault(t => t.Id.Equals(value.Value)); + } + + public ICustomerOrConsultant GetCustomerOrConsultant(string id) + { + IdValue value = _idSerializer.Deserialize(id); + if (value.TypeName == "Consultant") + { + return GetConsultant(id); + } + return GetCustomer(id); + } + } +} diff --git a/oxford_usergroup_2019/7_stitching/CustomerSchema/QueryType.cs b/oxford_usergroup_2019/7_stitching/CustomerSchema/QueryType.cs new file mode 100644 index 0000000..183bae0 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/CustomerSchema/QueryType.cs @@ -0,0 +1,27 @@ +using HotChocolate.Types; + +namespace Demo.Customers +{ + public class QueryType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetCustomer(default)) + .Argument("id", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.GetCustomers()) + .Type>>>(); + + descriptor.Field(t => t.GetConsultant(default)) + .Argument("id", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.GetCustomerOrConsultant(default)) + .Argument("id", a => a.Type>()) + .Type(); + } + } +} diff --git a/oxford_usergroup_2019/7_stitching/CustomerSchema/Startup.cs b/oxford_usergroup_2019/7_stitching/CustomerSchema/Startup.cs new file mode 100644 index 0000000..4bcace5 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/CustomerSchema/Startup.cs @@ -0,0 +1,35 @@ +using HotChocolate; +using HotChocolate.AspNetCore; +using Demo.Customers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Demo.Customers +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + + // Add GraphQL Services + services.AddGraphQL(Schema.Create(c => + { + c.RegisterQueryType(); + c.UseGlobalObjectIdentifier(); + })); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(); + app.UsePlayground(); + } + } +} diff --git a/oxford_usergroup_2019/7_stitching/CustomerSchema/customer.csproj b/oxford_usergroup_2019/7_stitching/CustomerSchema/customer.csproj new file mode 100644 index 0000000..c36ea18 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/CustomerSchema/customer.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp2.1 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + + + \ No newline at end of file diff --git a/oxford_usergroup_2019/7_stitching/Gateway/.vscode/launch.json b/oxford_usergroup_2019/7_stitching/Gateway/.vscode/launch.json new file mode 100644 index 0000000..2ed183d --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/Gateway/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.1/stitched.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart", + "launchBrowser": { + "enabled": true, + "args": "${auto-detect-url}", + "windows": { + "command": "cmd.exe", + "args": "/C start ${auto-detect-url}" + }, + "osx": { + "command": "open" + }, + "linux": { + "command": "xdg-open" + } + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/oxford_usergroup_2019/7_stitching/Gateway/.vscode/tasks.json b/oxford_usergroup_2019/7_stitching/Gateway/.vscode/tasks.json new file mode 100644 index 0000000..a914fef --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/Gateway/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet build", + "type": "shell", + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/oxford_usergroup_2019/7_stitching/Gateway/Extensions.graphql b/oxford_usergroup_2019/7_stitching/Gateway/Extensions.graphql new file mode 100644 index 0000000..abc38a6 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/Gateway/Extensions.graphql @@ -0,0 +1,9 @@ +type Query { + me: Customer + @delegate(schema: "customer", path: "customer(id:$contextData:userId)") +} + +extend type Customer { + contracts: [Contract!] + @delegate(schema: "contract", path: "contracts(customerId:$fields:id)") +} diff --git a/oxford_usergroup_2019/7_stitching/Gateway/Program.cs b/oxford_usergroup_2019/7_stitching/Gateway/Program.cs new file mode 100644 index 0000000..8086d15 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/Gateway/Program.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace Demo.Stitching +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseUrls("http://localhost:5055") + .UseStartup(); + } +} diff --git a/oxford_usergroup_2019/7_stitching/Gateway/Startup.cs b/oxford_usergroup_2019/7_stitching/Gateway/Startup.cs new file mode 100644 index 0000000..2482ebc --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/Gateway/Startup.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.AspNetCore; +using HotChocolate.Execution; +using HotChocolate.Resolvers; +using HotChocolate.Stitching; +using HotChocolate.Types; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Demo.Stitching +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + // Setup the clients that shall be used to access the remote endpoints. + services.AddHttpClient("customer", (sp, client) => + { + // in order to pass on the token or any other headers to the backend schema use the IHttpContextAccessor + HttpContext context = sp.GetRequiredService().HttpContext; + client.BaseAddress = new Uri("http://127.0.0.1:5050"); + }); + + services.AddHttpClient("contract", (sp, client) => + { + // in order to pass on the token or any other headers to the backend schema use the IHttpContextAccessor + HttpContext context = sp.GetRequiredService().HttpContext; + client.BaseAddress = new Uri("http://127.0.0.1:5051"); + }); + + services.AddHttpContextAccessor(); + + services.AddQueryRequestInterceptor((context, builder, ct) => + { + builder.AddProperty("userId", "Q3VzdG9tZXIKZDI="); + return Task.CompletedTask; + }); + + services.AddStitchedSchema(builder => + { + builder.AddSchemaFromHttp("contract") + .AddSchemaFromHttp("customer") + .AddExtensionsFromFile("./Extensions.graphql") + .RenameType("LifeInsuranceContract", "LifeInsurance") + .IgnoreRootTypes(); + }); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(new QueryMiddlewareOptions { EnableSubscriptions = false }); + app.UsePlayground(); + } + } +} diff --git a/oxford_usergroup_2019/7_stitching/Gateway/stitched.csproj b/oxford_usergroup_2019/7_stitching/Gateway/stitched.csproj new file mode 100644 index 0000000..b8aef24 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/Gateway/stitched.csproj @@ -0,0 +1,35 @@ + + + + netcoreapp2.1 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + + + + + + + Always + + + + diff --git a/oxford_usergroup_2019/7_stitching/README.md b/oxford_usergroup_2019/7_stitching/README.md new file mode 100644 index 0000000..d807a0c --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/README.md @@ -0,0 +1,20 @@ +# Schema Stitching Example + +This example shows how you can implement as stitched schema with Hot Chocolate. + +This example consists of the following projects: + +- CustomerSchema + The customer schema contains a GraphQL server that serves up a schema around a customer entity. + +- ContractSchema + The contract schema contains a GraphQL server that serves up a schema that provides insurance contract entities that can be assoicated with customers. + +- Stitching + The stitching project contains a GraphQL server that stitches the former mentiond GraphQL schemas together. + +1. Start the customer and contract servers with `dotnet run` +2. When the former servers are running start the stitching server with `dotnet run` +3. Head over to `http://127.0.0.1/playground` and test out some queries. + +[Hot Chocolate Documentation](https://hotchocolate.io) diff --git a/oxford_usergroup_2019/7_stitching/Stitching.sln.sln b/oxford_usergroup_2019/7_stitching/Stitching.sln.sln new file mode 100644 index 0000000..ca605b6 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/Stitching.sln.sln @@ -0,0 +1,62 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "contract", "ContractSchema\contract.csproj", "{09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "customer", "CustomerSchema\customer.csproj", "{C538BC50-D306-4AE3-8FE4-38481C27427D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "stitched", "Gateway\stitched.csproj", "{E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|x64.ActiveCfg = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|x64.Build.0 = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|x86.ActiveCfg = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|x86.Build.0 = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|Any CPU.Build.0 = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|x64.ActiveCfg = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|x64.Build.0 = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|x86.ActiveCfg = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|x86.Build.0 = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|x64.ActiveCfg = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|x64.Build.0 = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|x86.Build.0 = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|Any CPU.Build.0 = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|x64.ActiveCfg = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|x64.Build.0 = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|x86.ActiveCfg = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|x86.Build.0 = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|x64.ActiveCfg = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|x64.Build.0 = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|x86.ActiveCfg = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|x86.Build.0 = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|Any CPU.Build.0 = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|x64.ActiveCfg = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|x64.Build.0 = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|x86.ActiveCfg = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/oxford_usergroup_2019/7_stitching/copy.txt b/oxford_usergroup_2019/7_stitching/copy.txt new file mode 100644 index 0000000..3d6f935 --- /dev/null +++ b/oxford_usergroup_2019/7_stitching/copy.txt @@ -0,0 +1,60 @@ +services.AddStitchedSchema(builder => builder + .AddSchemaFromHttp("customer") + .AddSchemaFromHttp("contract") + .AddExtensionsFromFile("./Extensions.graphql") + .AddSchemaConfiguration(c => + { + + })); + +// .IgnoreRootTypes() + .RenameType("LifeInsuranceContract", "LifeInsurance") + +extend type Query { + me: Customer + @delegate( + schema: "customer" + path: "customer(id:$contextData:currentUserId)" + ) +} + +extend type Customer { + contracts: [Contract!] + @delegate(schema: "contract", path: "contracts(customerId:$fields:id)") +} + +services.AddQueryRequestInterceptor((context, builder, cancellationToken) => +{ + builder.AddProperty("currentUserId", "Q3VzdG9tZXIKZDE="); + return Task.CompletedTask; +}); + +c.RegisterType(); +// custom resolver that depends on data from a remote schema. +c.Map(new FieldReference("Customer", "foo"), next => context => +{ + OrderedDictionary obj = context.Parent(); + context.Result = obj["name"] + "_" + obj["id"]; + return Task.CompletedTask; +}); + + +public class SomeOtherContractExtension + : ObjectTypeExtension + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name("SomeOtherContract"); + descriptor.Field("expiresInDays") + .Type>() + .Directive(new ComputedDirective { DependantOn = new NameString[] { "expiryDate" } }) + .Resolver(context => + { + var obj = context.Parent>(); + var serializedExpiryDate = obj["expiryDate"]; + var dateType = (ISerializableType)context.ObjectType.Fields["expiryDate"].Type; + var offset = (DateTimeOffset)dateType.Deserialize(serializedExpiryDate); + return offset.DateTime.Subtract(DateTime.UtcNow).Days; + }); + } + } diff --git a/oxford_usergroup_2019/8_testing/.vscode/launch.json b/oxford_usergroup_2019/8_testing/.vscode/launch.json new file mode 100644 index 0000000..cc09e8b --- /dev/null +++ b/oxford_usergroup_2019/8_testing/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/netcoreapp3.0/Testing.dll", + "args": [], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/oxford_usergroup_2019/8_testing/.vscode/tasks.json b/oxford_usergroup_2019/8_testing/.vscode/tasks.json new file mode 100644 index 0000000..31c32bd --- /dev/null +++ b/oxford_usergroup_2019/8_testing/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/oxford_usergroup_2019/8_testing/Testing.csproj b/oxford_usergroup_2019/8_testing/Testing.csproj new file mode 100644 index 0000000..43d1d5f --- /dev/null +++ b/oxford_usergroup_2019/8_testing/Testing.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.0 + _9_testing + + false + + + + + + + + + + + + diff --git a/oxford_usergroup_2019/8_testing/UnitTest1.cs b/oxford_usergroup_2019/8_testing/UnitTest1.cs new file mode 100644 index 0000000..e3df79c --- /dev/null +++ b/oxford_usergroup_2019/8_testing/UnitTest1.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Execution; +using HotChocolate.Types; +using Snapshooter.Xunit; +using Xunit; + +namespace Testing +{ + public class MyTests + { + + } +} diff --git a/oxford_usergroup_2019/8_testing/copy.txt b/oxford_usergroup_2019/8_testing/copy.txt new file mode 100644 index 0000000..a075329 --- /dev/null +++ b/oxford_usergroup_2019/8_testing/copy.txt @@ -0,0 +1,40 @@ +[Fact] +public void Schema_Snapshot() +{ + // arrange + // act + ISchema schema = SchemaBuilder.New() + .AddQueryType(d => d + .Name("Query") + .Field("foo") + .Resolver("bar")) + .Create(); + + // assert + schema.ToString().MatchSnapshot(); +} + +[Fact] +public async Task Schema_Integration_Test() +{ + // arrange + ISchema schema = SchemaBuilder.New() + .AddQueryType(d => + { + d.Name("Query"); + d.Field("foo").Resolver("bar"); + d.Field("baz").Resolver(DateTimeOffset.UtcNow); + }) + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + // act + IExecutionResult result = await executor.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ foo baz }") + .Create()); + + // assert + result.MatchSnapshot(matchOptions => matchOptions.IgnoreField("Data.baz")); +} diff --git a/oxford_usergroup_2019/GraphQL.pptx b/oxford_usergroup_2019/GraphQL.pptx new file mode 100644 index 0000000..6526831 Binary files /dev/null and b/oxford_usergroup_2019/GraphQL.pptx differ diff --git a/zurich_usergroup_2019/1_graphql_vs_rest/index.html b/zurich_usergroup_2019/1_graphql_vs_rest/index.html new file mode 100644 index 0000000..889c94b --- /dev/null +++ b/zurich_usergroup_2019/1_graphql_vs_rest/index.html @@ -0,0 +1,268 @@ + + + + + GraphQL vs. REST + + + +
        + + vs. + +
        +
        + Loading... +
        +
        +

        + An unsorted list of characters who played in the same films where Luke + Skywalker were present +

        +

        + Used and returned + characters which took + ms. +

        +
          +
          + + + diff --git a/zurich_usergroup_2019/2_hello_world/.vscode/launch.json b/zurich_usergroup_2019/2_hello_world/.vscode/launch.json new file mode 100644 index 0000000..c509473 --- /dev/null +++ b/zurich_usergroup_2019/2_hello_world/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp3.0/2_hello_world.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/zurich_usergroup_2019/2_hello_world/.vscode/tasks.json b/zurich_usergroup_2019/2_hello_world/.vscode/tasks.json new file mode 100644 index 0000000..31c32bd --- /dev/null +++ b/zurich_usergroup_2019/2_hello_world/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/zurich_usergroup_2019/2_hello_world/HelloWorld.csproj b/zurich_usergroup_2019/2_hello_world/HelloWorld.csproj new file mode 100644 index 0000000..b3d059f --- /dev/null +++ b/zurich_usergroup_2019/2_hello_world/HelloWorld.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.0 + _2_hello_world + + + + + + + diff --git a/zurich_usergroup_2019/2_hello_world/Program.cs b/zurich_usergroup_2019/2_hello_world/Program.cs new file mode 100644 index 0000000..fd016d1 --- /dev/null +++ b/zurich_usergroup_2019/2_hello_world/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace HelloWorld +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/zurich_usergroup_2019/2_hello_world/Query.cs b/zurich_usergroup_2019/2_hello_world/Query.cs new file mode 100644 index 0000000..b36653b --- /dev/null +++ b/zurich_usergroup_2019/2_hello_world/Query.cs @@ -0,0 +1,9 @@ +namespace HelloWorld +{ + public class Query + { + public string GetHello() => "World"; + + public string GetGreetings(string text) => text; + } +} diff --git a/zurich_usergroup_2019/2_hello_world/QueryType.cs b/zurich_usergroup_2019/2_hello_world/QueryType.cs new file mode 100644 index 0000000..4fdbedc --- /dev/null +++ b/zurich_usergroup_2019/2_hello_world/QueryType.cs @@ -0,0 +1,13 @@ +using HotChocolate.Types; + +namespace HelloWorld +{ + public class QueryType : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetHello()).Type>(); + // Note: GetGreetings is inferred + } + } +} diff --git a/zurich_usergroup_2019/2_hello_world/Startup.cs b/zurich_usergroup_2019/2_hello_world/Startup.cs new file mode 100644 index 0000000..56a9a4b --- /dev/null +++ b/zurich_usergroup_2019/2_hello_world/Startup.cs @@ -0,0 +1,24 @@ +using HotChocolate; +using HotChocolate.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace HelloWorld +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddGraphQL(sp => SchemaBuilder.New() + .AddQueryType() + .AddServices(sp) + .Create()); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseGraphQL(); + } + } +} diff --git a/zurich_usergroup_2019/2_hello_world/appsettings.Development.json b/zurich_usergroup_2019/2_hello_world/appsettings.Development.json new file mode 100644 index 0000000..a2880cb --- /dev/null +++ b/zurich_usergroup_2019/2_hello_world/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/zurich_usergroup_2019/2_hello_world/appsettings.json b/zurich_usergroup_2019/2_hello_world/appsettings.json new file mode 100644 index 0000000..81ff877 --- /dev/null +++ b/zurich_usergroup_2019/2_hello_world/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/zurich_usergroup_2019/2_hello_world/global.json b/zurich_usergroup_2019/2_hello_world/global.json new file mode 100644 index 0000000..79422f0 --- /dev/null +++ b/zurich_usergroup_2019/2_hello_world/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "3.0.100" + } +} diff --git a/zurich_usergroup_2019/3_operations/.vscode/launch.json b/zurich_usergroup_2019/3_operations/.vscode/launch.json new file mode 100644 index 0000000..d3f889e --- /dev/null +++ b/zurich_usergroup_2019/3_operations/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/StarWars/bin/Debug/netcoreapp3.0/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/zurich_usergroup_2019/3_operations/.vscode/tasks.json b/zurich_usergroup_2019/3_operations/.vscode/tasks.json new file mode 100644 index 0000000..383f2db --- /dev/null +++ b/zurich_usergroup_2019/3_operations/.vscode/tasks.json @@ -0,0 +1,25 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + "StarWars", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/zurich_usergroup_2019/3_operations/StarWars/.vscode/launch.json b/zurich_usergroup_2019/3_operations/StarWars/.vscode/launch.json new file mode 100644 index 0000000..401f807 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.1/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/zurich_usergroup_2019/3_operations/StarWars/.vscode/tasks.json b/zurich_usergroup_2019/3_operations/StarWars/.vscode/tasks.json new file mode 100644 index 0000000..68ae5ff --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet build", + "type": "shell", + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Data/CharacterRepository.cs b/zurich_usergroup_2019/3_operations/StarWars/Data/CharacterRepository.cs new file mode 100644 index 0000000..dd09134 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Data/CharacterRepository.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StarWars.Models; + +namespace StarWars.Data +{ + public class CharacterRepository + { + private Dictionary _characters; + private Dictionary _starships; + + public CharacterRepository() + { + _characters = CreateCharacters().ToDictionary(t => t.Id); + _starships = CreateStarships().ToDictionary(t => t.Id); + } + + public ICharacter GetHero(Episode episode) + { + if (episode == Episode.Empire) + { + return _characters["1000"]; + } + return _characters["2001"]; + } + + public ICharacter GetCharacter(string id) + { + if (_characters.TryGetValue(id, out ICharacter c)) + { + return c; + } + return null; + } + + public Human GetHuman(string id) + { + if (_characters.TryGetValue(id, out ICharacter c) + && c is Human h) + { + return h; + } + return null; + } + + public Droid GetDroid(string id) + { + if (_characters.TryGetValue(id, out ICharacter c) + && c is Droid d) + { + return d; + } + return null; + } + + public IEnumerable Search(string text) + { +#if ASPNETCLASSIC + IEnumerable filteredCharacters = _characters.Values + .Where(t => t.Name.Contains(text)); +#else + IEnumerable filteredCharacters = _characters.Values + .Where(t => t.Name.Contains(text, + StringComparison.OrdinalIgnoreCase)); +#endif + + foreach (ICharacter character in filteredCharacters) + { + yield return character; + } + +#if ASPNETCLASSIC + IEnumerable filteredStarships = _starships.Values + .Where(t => t.Name.Contains(text)); +#else + IEnumerable filteredStarships = _starships.Values + .Where(t => t.Name.Contains(text, + StringComparison.OrdinalIgnoreCase)); +#endif + + foreach (Starship starship in filteredStarships) + { + yield return starship; + } + } + + private static IEnumerable CreateCharacters() + { + yield return new Human + { + Id = "1000", + Name = "Luke Skywalker", + Friends = new[] { "1002", "1003", "2000", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Tatooine" + }; + + yield return new Human + { + Id = "1001", + Name = "Darth Vader", + Friends = new[] { "1004" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Tatooine" + }; + + yield return new Human + { + Id = "1002", + Name = "Han Solo", + Friends = new[] { "1000", "1003", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi } + }; + + yield return new Human + { + Id = "1003", + Name = "Leia Organa", + Friends = new[] { "1000", "1002", "2000", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + HomePlanet = "Alderaan" + }; + + yield return new Human + { + Id = "1004", + Name = "Wilhuff Tarkin", + Friends = new[] { "1001" }, + AppearsIn = new[] { Episode.NewHope } + }; + + yield return new Droid + { + Id = "2000", + Name = "C-3PO", + Friends = new[] { "1000", "1002", "1003", "2001" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + PrimaryFunction = "Protocol" + }; + + yield return new Droid + { + Id = "2001", + Name = "R2-D2", + Friends = new[] { "1000", "1002", "1003" }, + AppearsIn = new[] { Episode.NewHope, Episode.Empire, Episode.Jedi }, + PrimaryFunction = "Astromech" + }; + } + + private static IEnumerable CreateStarships() + { + yield return new Starship + { + Id = "3000", + Name = "TIE Advanced x1", + Length = 9.2 + }; + } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Data/ReviewRepository.cs b/zurich_usergroup_2019/3_operations/StarWars/Data/ReviewRepository.cs new file mode 100644 index 0000000..a586306 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Data/ReviewRepository.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using StarWars.Models; + +namespace StarWars.Data +{ + public class ReviewRepository + { + private readonly Dictionary> _data = + new Dictionary>(); + + public void AddReview(Episode episode, Review review) + { + if (!_data.TryGetValue(episode, out List reviews)) + { + reviews = new List(); + _data[episode] = reviews; + } + + reviews.Add(review); + } + + public IEnumerable GetReviews(Episode episode) + { + if (_data.TryGetValue(episode, out List reviews)) + { + return reviews; + } + return Array.Empty(); + } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Models/Droid.cs b/zurich_usergroup_2019/3_operations/StarWars/Models/Droid.cs new file mode 100644 index 0000000..0ee4213 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Models/Droid.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A droid in the Star Wars universe. + /// + public class Droid + : ICharacter + { + /// + public string Id { get; set; } + + /// + public string Name { get; set; } + + /// + public IReadOnlyList Friends { get; set; } + + /// + public IReadOnlyList AppearsIn { get; set; } + + /// + /// The droid's primary function. + /// + public string PrimaryFunction { get; set; } + + /// + public double Height { get; } = 1.72d; + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Models/Episode.cs b/zurich_usergroup_2019/3_operations/StarWars/Models/Episode.cs new file mode 100644 index 0000000..6900cf6 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Models/Episode.cs @@ -0,0 +1,21 @@ +namespace StarWars.Models +{ + /// + /// The Star Wars episodes. + /// + public enum Episode + { + /// + /// Star Wars Episode IV: A New Hope + /// + NewHope, + /// + /// Star Wars Episode V: Empire Strikes Back + /// + Empire, + /// + /// Star Wars Episode VI: Return of the Jedi + /// + Jedi + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Models/Human.cs b/zurich_usergroup_2019/3_operations/StarWars/Models/Human.cs new file mode 100644 index 0000000..caf6021 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Models/Human.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A human character in the Star Wars universe. + /// + public class Human + : ICharacter + { + /// + public string Id { get; set; } + + /// + public string Name { get; set; } + + /// + public IReadOnlyList Friends { get; set; } + + /// + public IReadOnlyList AppearsIn { get; set; } + + /// + /// The planet the character is originally from. + /// + public string HomePlanet { get; set; } + + /// + public double Height { get; } = 1.72d; + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Models/ICharacter.cs b/zurich_usergroup_2019/3_operations/StarWars/Models/ICharacter.cs new file mode 100644 index 0000000..f9186c1 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Models/ICharacter.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace StarWars.Models +{ + /// + /// A character in the Star Wars universe. + /// + public interface ICharacter + { + /// + /// The unique identifier for the character. + /// + string Id { get; } + + /// + /// The name of the character. + /// + string Name { get; } + + /// + /// The names of the character's friends. + /// + IReadOnlyList Friends { get; } + + /// + /// The episodes the character appears in. + /// + IReadOnlyList AppearsIn { get; } + + /// + /// The height of the character. + /// + double Height { get; } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Models/Review.cs b/zurich_usergroup_2019/3_operations/StarWars/Models/Review.cs new file mode 100644 index 0000000..3f18f16 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Models/Review.cs @@ -0,0 +1,18 @@ +namespace StarWars.Models +{ + /// + /// A review of a particular movie. + /// + public class Review + { + /// + /// The number of stars given for this review. + /// + public int Stars { get; set; } + + /// + /// An explanation for the rating. + /// + public string Commentary { get; set; } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Models/Starship.cs b/zurich_usergroup_2019/3_operations/StarWars/Models/Starship.cs new file mode 100644 index 0000000..5d7c241 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Models/Starship.cs @@ -0,0 +1,23 @@ +namespace StarWars.Models +{ + /// + /// A starship in the Star Wars universe. + /// + public class Starship + { + /// + /// The Id of the starship. + /// + public string Id { get; set; } + + /// + /// The name of the starship. + /// + public string Name { get; set; } + + /// + /// The length of the starship. + /// + public double Length { get; set; } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Models/Unit.cs b/zurich_usergroup_2019/3_operations/StarWars/Models/Unit.cs new file mode 100644 index 0000000..15316aa --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Models/Unit.cs @@ -0,0 +1,11 @@ +namespace StarWars.Models +{ + /// + /// Different units of measurement. + /// + public enum Unit + { + Foot, + Meters + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Mutation.cs b/zurich_usergroup_2019/3_operations/StarWars/Mutation.cs new file mode 100644 index 0000000..45d4adc --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Mutation.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Subscriptions; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Mutation + { + private readonly ReviewRepository _repository; + + public Mutation(ReviewRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Creates a review for a given Star Wars episode. + /// + /// The episode to review. + /// The review. + /// The event sending service. + /// The created review. + public async Task CreateReview( + Episode episode, Review review, + [Service]IEventSender eventSender) + { + _repository.AddReview(episode, review); + await eventSender.SendAsync(new OnReviewMessage(episode, review)); + return review; + } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/OnReviewMessage.cs b/zurich_usergroup_2019/3_operations/StarWars/OnReviewMessage.cs new file mode 100644 index 0000000..95f9cff --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/OnReviewMessage.cs @@ -0,0 +1,23 @@ +using HotChocolate.Language; +using HotChocolate.Subscriptions; +using StarWars.Models; + +namespace StarWars +{ + public class OnReviewMessage + : EventMessage + { + public OnReviewMessage(Episode episode, Review review) + : base(CreateEventDescription(episode), review) + { + } + + private static EventDescription CreateEventDescription(Episode episode) + { + return new EventDescription("onReview", + new ArgumentNode("episode", + new EnumValueNode( + episode.ToString().ToUpperInvariant()))); + } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Program.cs b/zurich_usergroup_2019/3_operations/StarWars/Program.cs new file mode 100644 index 0000000..0358799 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace StarWars +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Query.cs b/zurich_usergroup_2019/3_operations/StarWars/Query.cs new file mode 100644 index 0000000..887534e --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Query.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using HotChocolate.Resolvers; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Query + { + private readonly CharacterRepository _repository; + + public Query(CharacterRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Retrieve a hero by a particular Star Wars episode. + /// + /// The episode to look up by. + /// The character. + public ICharacter GetHero(Episode episode) + { + return _repository.GetHero(episode); + } + + /// + /// Gets a human by Id. + /// + /// The Id of the human to retrieve. + /// The human. + public Human GetHuman(string id) + { + return _repository.GetHuman(id); + } + + /// + /// Get a particular droid by Id. + /// + /// The Id of the droid. + /// The droid. + public Droid GetDroid(string id) + { + return _repository.GetDroid(id); + } + + public IEnumerable GetCharacter(string[] characterIds, IResolverContext context) + { + foreach (string characterId in characterIds) + { + ICharacter character = _repository.GetCharacter(characterId); + if (character == null) + { + context.ReportError( + "Could not resolve a charachter for the " + + $"character-id {characterId}."); + } + else + { + yield return character; + } + } + } + + public IEnumerable Search(string text) + { + return _repository.Search(text); + } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Resolvers/SharedResolvers.cs b/zurich_usergroup_2019/3_operations/StarWars/Resolvers/SharedResolvers.cs new file mode 100644 index 0000000..3911604 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Resolvers/SharedResolvers.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using HotChocolate; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars.Resolvers +{ + public class SharedResolvers + { + public IEnumerable GetCharacter( + [Parent]ICharacter character, + [Service]CharacterRepository repository) + { + foreach (string friendId in character.Friends) + { + ICharacter friend = repository.GetCharacter(friendId); + if (friend != null) + { + yield return friend; + } + } + } + + public double GetHeight(Unit? unit, [Parent]ICharacter character) + => ConvertToUnit(character.Height, unit); + + public double GetLength(Unit? unit, [Parent]Starship starship) + => ConvertToUnit(starship.Length, unit); + + private double ConvertToUnit(double length, Unit? unit) + { + if (unit == Unit.Foot) + { + return length * 3.28084d; + } + return length; + } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/StarWars.csproj b/zurich_usergroup_2019/3_operations/StarWars/StarWars.csproj new file mode 100644 index 0000000..1b041de --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/StarWars.csproj @@ -0,0 +1,34 @@ + + + + netcoreapp3.0 + false + 7.2 + true + $(NoWarn);1591 + + + + portable + true + + + + pdbonly + true + + + + + + + + + + + + + + + + diff --git a/zurich_usergroup_2019/3_operations/StarWars/Startup.cs b/zurich_usergroup_2019/3_operations/StarWars/Startup.cs new file mode 100644 index 0000000..b55e4d8 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Startup.cs @@ -0,0 +1,66 @@ +using System.Security.Claims; +using HotChocolate; +using HotChocolate.AspNetCore; +using HotChocolate.AspNetCore.Voyager; +using HotChocolate.Execution.Configuration; +using HotChocolate.Subscriptions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using StarWars.Data; +using StarWars.Types; + +namespace StarWars +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + // Add the custom services like repositories etc ... + services.AddSingleton(); + services.AddSingleton(); + + // Add in-memory event provider + services.AddInMemorySubscriptionProvider(); + + // Add GraphQL Services + services.AddGraphQL(sp => SchemaBuilder.New() + .AddServices(sp) + + // Adds the authorize directive and + // enable the authorization middleware. + .AddAuthorizeDirectiveType() + + .AddQueryType() + .AddMutationType() + .AddSubscriptionType() + .AddType() + .AddType() + .AddType() + .Create()); + + + // Add Authorization Policy + services.AddAuthorization(options => + { + options.AddPolicy("HasCountry", policy => + policy.RequireAssertion(context => + context.User.HasClaim(c => + (c.Type == ClaimTypes.Country)))); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app + .UseWebSockets() + .UseGraphQL("/graphql") + .UseGraphiQL("/graphql") + .UsePlayground("/graphql") + .UseVoyager("/graphql"); + } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Subscription.cs b/zurich_usergroup_2019/3_operations/StarWars/Subscription.cs new file mode 100644 index 0000000..06a88b2 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Subscription.cs @@ -0,0 +1,23 @@ +using System; +using HotChocolate.Subscriptions; +using StarWars.Data; +using StarWars.Models; + +namespace StarWars +{ + public class Subscription + { + private readonly ReviewRepository _repository; + + public Subscription(ReviewRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + public Review OnReview(Episode episode, IEventMessage message) + { + return (Review)message.Payload; + } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Types/CharacterType.cs b/zurich_usergroup_2019/3_operations/StarWars/Types/CharacterType.cs new file mode 100644 index 0000000..b9c5c98 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Types/CharacterType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; + +namespace StarWars.Types +{ + public class CharacterType + : InterfaceType + { + protected override void Configure(IInterfaceTypeDescriptor descriptor) + { + descriptor.Name("Character"); + + descriptor.Field(f => f.Id) + .Type>(); + + descriptor.Field(f => f.Name) + .Type(); + + descriptor.Field(f => f.Friends) + .UsePaging(); + + descriptor.Field(f => f.AppearsIn) + .Type>(); + + descriptor.Field(f => f.Height) + .Type() + .Argument("unit", a => a.Type>()); + } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Types/DroidType.cs b/zurich_usergroup_2019/3_operations/StarWars/Types/DroidType.cs new file mode 100644 index 0000000..c34fe9a --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Types/DroidType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class DroidType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.AppearsIn) + .Type>(); + + descriptor.Field(r => r.GetCharacter(default, default)) + .UsePaging() + .Name("friends"); + + descriptor.Field(t => t.GetHeight(default, default)) + .Type() + .Argument("unit", a => a.Type>()) + .Name("height"); + } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Types/EpisodeType.cs b/zurich_usergroup_2019/3_operations/StarWars/Types/EpisodeType.cs new file mode 100644 index 0000000..bfb484d --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Types/EpisodeType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class EpisodeType + : EnumType + { + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Types/HumanType.cs b/zurich_usergroup_2019/3_operations/StarWars/Types/HumanType.cs new file mode 100644 index 0000000..3ab1c6a --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Types/HumanType.cs @@ -0,0 +1,31 @@ +using HotChocolate.Types; +using HotChocolate.Types.Relay; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class HumanType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.AppearsIn) + .Type>(); + + descriptor.Field(r => r.GetCharacter(default, default)) + .UsePaging() + .Name("friends"); + + descriptor.Field(t => t.GetHeight(default, default)) + .Type() + .Argument("unit", a => a.Type>()) + .Name("height"); + } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Types/MutationType.cs b/zurich_usergroup_2019/3_operations/StarWars/Types/MutationType.cs new file mode 100644 index 0000000..71963b7 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Types/MutationType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Types; + +namespace StarWars.Types +{ + public class MutationType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.CreateReview(default, default, default)) + .Type>() + .Argument("episode", a => a.Type>()) + .Argument("review", a => a.Type>()); + } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Types/QueryType.cs b/zurich_usergroup_2019/3_operations/StarWars/Types/QueryType.cs new file mode 100644 index 0000000..193cf89 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Types/QueryType.cs @@ -0,0 +1,22 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class QueryType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetHero(default)) + .Type() + .Argument("episode", a => a.DefaultValue(Episode.NewHope)); + + descriptor.Field(t => t.GetCharacter(default, default)) + .Type>>>(); + + descriptor.Field(t => t.Search(default)) + .Type>(); + } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Types/ReviewInputType.cs b/zurich_usergroup_2019/3_operations/StarWars/Types/ReviewInputType.cs new file mode 100644 index 0000000..300fbe6 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Types/ReviewInputType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class ReviewInputType + : InputObjectType + { + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Types/ReviewType.cs b/zurich_usergroup_2019/3_operations/StarWars/Types/ReviewType.cs new file mode 100644 index 0000000..be94b38 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Types/ReviewType.cs @@ -0,0 +1,10 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class ReviewType + : ObjectType + { + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Types/SearchResultType.cs b/zurich_usergroup_2019/3_operations/StarWars/Types/SearchResultType.cs new file mode 100644 index 0000000..7d1bb88 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Types/SearchResultType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Types; +using StarWars.Models; + +namespace StarWars.Types +{ + public class SearchResultType + : UnionType + { + protected override void Configure(IUnionTypeDescriptor descriptor) + { + descriptor.Name("SearchResult"); + descriptor.Type>(); + descriptor.Type(); + descriptor.Type(); + } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Types/StarshipType.cs b/zurich_usergroup_2019/3_operations/StarWars/Types/StarshipType.cs new file mode 100644 index 0000000..7ad6b10 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Types/StarshipType.cs @@ -0,0 +1,18 @@ +using HotChocolate.Types; +using StarWars.Models; +using StarWars.Resolvers; + +namespace StarWars.Types +{ + public class StarshipType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.GetLength(default, default)); + } + } +} diff --git a/zurich_usergroup_2019/3_operations/StarWars/Types/SubscriptionType.cs b/zurich_usergroup_2019/3_operations/StarWars/Types/SubscriptionType.cs new file mode 100644 index 0000000..ed872e5 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/StarWars/Types/SubscriptionType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace StarWars.Types +{ + public class SubscriptionType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.OnReview(default, default)) + .Type>() + .Argument("episode", arg => arg.Type>()); + } + } +} diff --git a/zurich_usergroup_2019/3_operations/global.json b/zurich_usergroup_2019/3_operations/global.json new file mode 100644 index 0000000..79422f0 --- /dev/null +++ b/zurich_usergroup_2019/3_operations/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "3.0.100" + } +} diff --git a/zurich_usergroup_2019/4_dataloader/DataLoader.csproj b/zurich_usergroup_2019/4_dataloader/DataLoader.csproj new file mode 100644 index 0000000..1dd0b2f --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/DataLoader.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp2.2 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + + + diff --git a/zurich_usergroup_2019/4_dataloader/Message.cs b/zurich_usergroup_2019/4_dataloader/Message.cs new file mode 100644 index 0000000..5a16590 --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/Message.cs @@ -0,0 +1,16 @@ +using System; +using MongoDB.Bson; +using HotChocolate.Language; + +namespace HotChocolate.Examples.Paging +{ + public class Message + { + public ObjectId Id { get; set; } + public string Text { get; set; } + public DateTimeOffset Created { get; set; } + public int Favorites { get; set; } + public ObjectId UserId { get; set; } + public ObjectId? ReplyToId { get; set; } + } +} diff --git a/zurich_usergroup_2019/4_dataloader/MessageInput.cs b/zurich_usergroup_2019/4_dataloader/MessageInput.cs new file mode 100644 index 0000000..22b7823 --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/MessageInput.cs @@ -0,0 +1,11 @@ +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class MessageInput + { + public string Text { get; set; } + public ObjectId UserId { get; set; } + public ObjectId? ReplyToId { get; set; } + } +} diff --git a/zurich_usergroup_2019/4_dataloader/MessageInputType.cs b/zurich_usergroup_2019/4_dataloader/MessageInputType.cs new file mode 100644 index 0000000..249be7d --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/MessageInputType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class MessageInputType + : InputObjectType + { + protected override void Configure(IInputObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Text).Type>(); + descriptor.Field(t => t.UserId).Type>(); + descriptor.Field(t => t.ReplyToId).Type(); + } + } +} diff --git a/zurich_usergroup_2019/4_dataloader/MessageRepository.cs b/zurich_usergroup_2019/4_dataloader/MessageRepository.cs new file mode 100644 index 0000000..ad224ae --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/MessageRepository.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace HotChocolate.Examples.Paging +{ + public class MessageRepository + { + private readonly IMongoCollection _messageCollection; + + public MessageRepository(IMongoCollection messageCollection) + { + _messageCollection = messageCollection + ?? throw new ArgumentNullException(nameof(messageCollection)); + } + + public IQueryable GetAllMessages() + { + return _messageCollection.AsQueryable(); + } + + public Task GetMessageById(ObjectId messageId) + { + return _messageCollection.AsQueryable().FirstOrDefaultAsync(t => t.Id == messageId); + } + + public Task CreateMessageAsync(Message message, CancellationToken cancellationToken) + { + return _messageCollection.InsertOneAsync(message, new InsertOneOptions(), cancellationToken); + } + } +} diff --git a/zurich_usergroup_2019/4_dataloader/MessageType.cs b/zurich_usergroup_2019/4_dataloader/MessageType.cs new file mode 100644 index 0000000..4b9fb1d --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/MessageType.cs @@ -0,0 +1,44 @@ +using HotChocolate.Resolvers; +using HotChocolate.Types; +using GreenDonut; +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class MessageType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Text).Type>(); + descriptor.Field("createdBy").Type>().Resolver(ctx => + { + UserRepository repository = ctx.Service(); + + IDataLoader dataLoader = ctx.BatchDataLoader( + "UserById", + repository.GetUsersAsync); + + return dataLoader.LoadAsync(ctx.Parent().UserId); + }); + descriptor.Field("replyTo").Type().Resolver(async ctx => + { + ObjectId? replyToId = ctx.Parent().ReplyToId; + if (replyToId.HasValue) + { + MessageRepository repository = ctx.Service(); + + IDataLoader dataLoader = ctx.CacheDataLoader( + "MessageById", + repository.GetMessageById); + + return await dataLoader.LoadAsync(ctx.Parent().ReplyToId.Value); + } + return null; + }); + descriptor.Ignore(t => t.UserId); + descriptor.Ignore(t => t.ReplyToId); + } + } +} diff --git a/zurich_usergroup_2019/4_dataloader/Mutation.cs b/zurich_usergroup_2019/4_dataloader/Mutation.cs new file mode 100644 index 0000000..2aae183 --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/Mutation.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace HotChocolate.Examples.Paging +{ + public class Mutation + { + public async Task CreateMessageAsync( + MessageInput messageInput, + [Service]MessageRepository repository, + CancellationToken cancellationToken) + { + var message = new Message + { + Text = messageInput.Text, + UserId = messageInput.UserId, + ReplyToId = messageInput.ReplyToId, + Created = DateTimeOffset.UtcNow, + }; + + await repository.CreateMessageAsync(message, cancellationToken); + + return message; + } + + public async Task CreateUserAsync( + UserInput userInput, + [Service]UserRepository repository, + CancellationToken cancellationToken) + { + var user = new User + { + Name = userInput.Name, + Country = userInput.Country + }; + + await repository.CreateUserAsync(user, cancellationToken); + + return user; + } + } +} diff --git a/zurich_usergroup_2019/4_dataloader/MutationType.cs b/zurich_usergroup_2019/4_dataloader/MutationType.cs new file mode 100644 index 0000000..18bfcef --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/MutationType.cs @@ -0,0 +1,19 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class MutationType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.CreateMessageAsync(default, default, default)) + .Argument("messageInput", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.CreateUserAsync(default, default, default)) + .Argument("userInput", a => a.Type>()) + .Type(); + } + } +} diff --git a/zurich_usergroup_2019/4_dataloader/Program.cs b/zurich_usergroup_2019/4_dataloader/Program.cs new file mode 100644 index 0000000..3c1cee5 --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace HotChocolate.Examples.Paging +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/zurich_usergroup_2019/4_dataloader/Query.cs b/zurich_usergroup_2019/4_dataloader/Query.cs new file mode 100644 index 0000000..c3d5ae6 --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/Query.cs @@ -0,0 +1,12 @@ +using System.Linq; + +namespace HotChocolate.Examples.Paging +{ + public class Query + { + public IQueryable GetMessages([Service]MessageRepository repository) + { + return repository.GetAllMessages(); + } + } +} diff --git a/zurich_usergroup_2019/4_dataloader/QueryType.cs b/zurich_usergroup_2019/4_dataloader/QueryType.cs new file mode 100644 index 0000000..b44ce08 --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/QueryType.cs @@ -0,0 +1,34 @@ +using GreenDonut; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using HotChocolate.Types.Relay; + +namespace HotChocolate.Examples.Paging +{ + public class QueryType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetMessages(default)) + .UsePaging() + .UseFiltering() + .UseSorting(); + + descriptor.Field("usersByCountry") + .Argument("country", a => a.Type>()) + .Type>>>() + .Resolver(ctx => + { + var userRepository = ctx.Service(); + + IDataLoader userDataLoader = + ctx.GroupDataLoader( + "usersByCountry", + userRepository.GetUsersByCountry); + + return userDataLoader.LoadAsync(ctx.Argument("country")); + }); + } + } +} diff --git a/zurich_usergroup_2019/4_dataloader/README.md b/zurich_usergroup_2019/4_dataloader/README.md new file mode 100644 index 0000000..760b464 --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/README.md @@ -0,0 +1,21 @@ +# DataLoader Example + +This example shows how DataLoader can be used with Hot Chocolate and uses _Mongo_ as database. + +The **Cache DataLoader** and the **Batch DataLoader** are used in `MessageType.cs`. + +The **GroupDataLoader** is used in `QueryType.cs`. + +We support more DataLoader scenarious with Hot Chocolate than are showcased with this example. The example is aimed to show the most common use-cases. + +## Setup Mongo + +Personally I used docker to host my mongo db for the example. If you have setup docker that just add the following line in your terminal emulator of choice: + +```bash +docker run --name mongo -p 27017:27017 -d mongo mongod +``` + +If you don't have docker or do not want to use it you can install mongo from here: [https://www.mongodb.com/download-center/community](https://www.mongodb.com/download-center/community). + +[Hot Chocolate Documentation](https://hotchocolate.io) diff --git a/zurich_usergroup_2019/4_dataloader/Startup.cs b/zurich_usergroup_2019/4_dataloader/Startup.cs new file mode 100644 index 0000000..d445da4 --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/Startup.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.AspNetCore; +using HotChocolate; +using HotChocolate.Execution.Configuration; +using MongoDB.Driver; +using HotChocolate.Utilities; +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + // setup type conversion for object id + TypeConversion.Default.Register(from => ObjectId.Parse(from)); + TypeConversion.Default.Register(from => from.ToString()); + + // setup the repositories + services.AddSingleton(new MongoClient("mongodb://127.0.0.1:27017")); + services.AddSingleton(s => s.GetRequiredService().GetDatabase("PagingDemo")); + services.AddSingleton>(s => s.GetRequiredService().GetCollection("messages")); + services.AddSingleton>(s => s.GetRequiredService().GetCollection("users")); + services.AddSingleton(); + services.AddSingleton(); + + // this enables you to use DataLoader in your resolvers. + services.AddDataLoaderRegistry(); + + // Add GraphQL Services + services.AddGraphQL(SchemaBuilder.New() + .AddQueryType() + .AddMutationType()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(); + app.UsePlayground(); + } + } +} diff --git a/zurich_usergroup_2019/4_dataloader/User.cs b/zurich_usergroup_2019/4_dataloader/User.cs new file mode 100644 index 0000000..155c78b --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/User.cs @@ -0,0 +1,11 @@ +using MongoDB.Bson; + +namespace HotChocolate.Examples.Paging +{ + public class User + { + public ObjectId Id { get; set; } + public string Name { get; set; } + public string Country { get; set; } + } +} diff --git a/zurich_usergroup_2019/4_dataloader/UserInput.cs b/zurich_usergroup_2019/4_dataloader/UserInput.cs new file mode 100644 index 0000000..2511647 --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/UserInput.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.Examples.Paging +{ + public class UserInput + { + public string Name { get; set; } + public string Country { get; set; } + } +} diff --git a/zurich_usergroup_2019/4_dataloader/UserInputType.cs b/zurich_usergroup_2019/4_dataloader/UserInputType.cs new file mode 100644 index 0000000..1e38cc0 --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/UserInputType.cs @@ -0,0 +1,14 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class UserInputType + : InputObjectType + { + protected override void Configure(IInputObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Name).Type>(); + descriptor.Field(t => t.Country).Type>(); + } + } +} diff --git a/zurich_usergroup_2019/4_dataloader/UserRepository.cs b/zurich_usergroup_2019/4_dataloader/UserRepository.cs new file mode 100644 index 0000000..90399ed --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/UserRepository.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace HotChocolate.Examples.Paging +{ + public class UserRepository + { + private readonly IMongoCollection _userCollection; + + public UserRepository(IMongoCollection userCollection) + { + _userCollection = userCollection + ?? throw new ArgumentNullException(nameof(userCollection)); + } + + public IQueryable GetAllUsers() + { + return _userCollection.AsQueryable(); + } + + public async Task> GetUsersByCountry( + IReadOnlyList countries, + CancellationToken cancellationToken) + { + var filters = new List>(); + + foreach (string country in countries) + { + filters.Add(Builders.Filter.Eq(u => u.Country, country)); + } + + List users = await _userCollection + .Find(Builders.Filter.Or(filters)) + .ToListAsync(cancellationToken); + + return users.ToLookup(t => t.Country); + } + + public Task GetUserAsync(ObjectId userId, CancellationToken cancellationToken) + { + return _userCollection.Find(c => c.Id == userId) + .FirstOrDefaultAsync(cancellationToken); + } + + public Task CreateUserAsync(User user, CancellationToken cancellationToken) + { + return _userCollection.InsertOneAsync(user, new InsertOneOptions(), cancellationToken); + } + + public async Task> GetUsersAsync( + IReadOnlyCollection userIds, + CancellationToken cancellationToken) + { + var filters = new List>(); + foreach (ObjectId userId in userIds) + { + filters.Add(Builders.Filter.Eq(u => u.Id, userId)); + } + + List users = await _userCollection + .Find(Builders.Filter.Or(filters)) + .ToListAsync(cancellationToken); + + return users.ToDictionary(t => t.Id); + } + } +} diff --git a/zurich_usergroup_2019/4_dataloader/UserType.cs b/zurich_usergroup_2019/4_dataloader/UserType.cs new file mode 100644 index 0000000..7af7a05 --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/UserType.cs @@ -0,0 +1,14 @@ +using HotChocolate.Types; + +namespace HotChocolate.Examples.Paging +{ + public class UserType + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Name).Type>(); + } + } +} diff --git a/zurich_usergroup_2019/4_dataloader/copy.txt b/zurich_usergroup_2019/4_dataloader/copy.txt new file mode 100644 index 0000000..4248412 --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/copy.txt @@ -0,0 +1,23 @@ +MessageType + +descriptor.Field("createdBy").Type>().Resolver(ctx => +{ + UserRepository repository = ctx.Service(); + + IDataLoader dataLoader = ctx.BatchDataLoader( + "UserById", + repository.GetUsersAsync); + + return dataLoader.LoadAsync(ctx.Parent().UserId); +}); + + +IDataLoader dataLoader = ctx.CacheDataLoader( + "MessageById", + repository.GetMessageById); + +return await dataLoader.LoadAsync(ctx.Parent().ReplyToId.Value); + +.UsePaging() + +docker run --name mongo -p 27017:27017 -d mongo mongod \ No newline at end of file diff --git a/zurich_usergroup_2019/4_dataloader/global.json b/zurich_usergroup_2019/4_dataloader/global.json new file mode 100644 index 0000000..89b3b0f --- /dev/null +++ b/zurich_usergroup_2019/4_dataloader/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "2.2.402" + } +} diff --git a/zurich_usergroup_2019/5_stitching/.vscode/launch.json b/zurich_usergroup_2019/5_stitching/.vscode/launch.json new file mode 100644 index 0000000..d883fe4 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/Gateway/bin/Debug/netcoreapp2.1/stitched.dll", + "args": [], + "cwd": "${workspaceFolder}/Gateway", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/zurich_usergroup_2019/5_stitching/.vscode/tasks.json b/zurich_usergroup_2019/5_stitching/.vscode/tasks.json new file mode 100644 index 0000000..31c32bd --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/zurich_usergroup_2019/5_stitching/ContractSchema/ContractStorage.cs b/zurich_usergroup_2019/5_stitching/ContractSchema/ContractStorage.cs new file mode 100644 index 0000000..57295c7 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/ContractSchema/ContractStorage.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Demo.Contracts +{ + public class ContractStorage + { + public List Contracts { get; } = new List + { + new LifeInsuranceContract + { + Id = "1", + CustomerId= "1", + Premium = 123456.11 + }, + new LifeInsuranceContract + { + Id = "2", + CustomerId= "1", + Premium = 456789.12 + }, + new LifeInsuranceContract + { + Id = "3", + CustomerId = "2", + Premium = 789.12 + }, + new SomeOtherContract + { + Id = "1", + CustomerId= "1", + ExpiryDate = new DateTime(2015, 2, 1, 0,0,0, DateTimeKind.Utc) + }, + new SomeOtherContract + { + Id = "2", + CustomerId= "2", + ExpiryDate = new DateTime(2015, 5, 1, 0,0,0, DateTimeKind.Utc) + }, + new SomeOtherContract + { + Id = "3", + CustomerId= "3", + ExpiryDate = new DateTime(2017, 1, 30, 0,0,0, DateTimeKind.Utc) + }, + new SomeOtherContract + { + Id = "4", + CustomerId= "3", + ExpiryDate = new DateTime(2020, 1, 1, 0,0,0, DateTimeKind.Utc) + } + }; + } +} diff --git a/zurich_usergroup_2019/5_stitching/ContractSchema/ContractType.cs b/zurich_usergroup_2019/5_stitching/ContractSchema/ContractType.cs new file mode 100644 index 0000000..ff5b38b --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/ContractSchema/ContractType.cs @@ -0,0 +1,14 @@ +using HotChocolate.Types; + +namespace Demo.Contracts +{ + public class ContractType + : InterfaceType + { + protected override void Configure(IInterfaceTypeDescriptor descriptor) + { + descriptor.Name("Contract"); + descriptor.Field("id").Type>(); + } + } +} diff --git a/zurich_usergroup_2019/5_stitching/ContractSchema/IContract.cs b/zurich_usergroup_2019/5_stitching/ContractSchema/IContract.cs new file mode 100644 index 0000000..64397c4 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/ContractSchema/IContract.cs @@ -0,0 +1,9 @@ +namespace Demo.Contracts +{ + public interface IContract + { + string Id { get; } + + string CustomerId { get; } + } +} diff --git a/zurich_usergroup_2019/5_stitching/ContractSchema/LifeInsuranceContract.cs b/zurich_usergroup_2019/5_stitching/ContractSchema/LifeInsuranceContract.cs new file mode 100644 index 0000000..93a577a --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/ContractSchema/LifeInsuranceContract.cs @@ -0,0 +1,12 @@ +namespace Demo.Contracts +{ + public class LifeInsuranceContract + : IContract + { + public string Id { get; set; } + + public string CustomerId { get; set; } + + public double Premium { get; set; } + } +} diff --git a/zurich_usergroup_2019/5_stitching/ContractSchema/LifeInsuranceContractType.cs b/zurich_usergroup_2019/5_stitching/ContractSchema/LifeInsuranceContractType.cs new file mode 100644 index 0000000..c629ecf --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/ContractSchema/LifeInsuranceContractType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Types; + +namespace Demo.Contracts +{ + public class LifeInsuranceContractType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.CustomerId).Ignore(); + } + } +} diff --git a/zurich_usergroup_2019/5_stitching/ContractSchema/Program.cs b/zurich_usergroup_2019/5_stitching/ContractSchema/Program.cs new file mode 100644 index 0000000..4ec4019 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/ContractSchema/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Demo.Contracts +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseUrls("http://localhost:5051") + .UseStartup(); + } +} diff --git a/zurich_usergroup_2019/5_stitching/ContractSchema/Query.cs b/zurich_usergroup_2019/5_stitching/ContractSchema/Query.cs new file mode 100644 index 0000000..363b8f7 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/ContractSchema/Query.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HotChocolate.Types.Relay; + +namespace Demo.Contracts +{ + public class Query + { + private readonly IdSerializer _idSerializer = new IdSerializer(); + private readonly ContractStorage _contractStorage; + + public Query(ContractStorage contractStorage) + { + _contractStorage = contractStorage + ?? throw new ArgumentNullException(nameof(contractStorage)); + } + + public IContract GetContract(string contractId) + { + IdValue value = _idSerializer.Deserialize(contractId); + + if (value.TypeName == nameof(LifeInsuranceContract)) + { + return _contractStorage.Contracts + .OfType() + .FirstOrDefault(t => t.Id.Equals(value.Value)); + } + else + { + return _contractStorage.Contracts + .OfType() + .FirstOrDefault(t => t.Id.Equals(value.Value)); + } + } + + public IEnumerable GetContracts(string customerId) + { + IdValue value = _idSerializer.Deserialize(customerId); + + return _contractStorage.Contracts + .Where(t => t.CustomerId.Equals(value.Value)); + } + } +} diff --git a/zurich_usergroup_2019/5_stitching/ContractSchema/QueryType.cs b/zurich_usergroup_2019/5_stitching/ContractSchema/QueryType.cs new file mode 100644 index 0000000..c012283 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/ContractSchema/QueryType.cs @@ -0,0 +1,20 @@ +using HotChocolate.Types; + +namespace Demo.Contracts +{ + public class QueryType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetContract(default)) + .Argument("contractId", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.GetContracts(default)) + .Argument("customerId", a => a.Type>()) + .Type>>(); + } + } +} diff --git a/zurich_usergroup_2019/5_stitching/ContractSchema/SomeOtherContract.cs b/zurich_usergroup_2019/5_stitching/ContractSchema/SomeOtherContract.cs new file mode 100644 index 0000000..54c1572 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/ContractSchema/SomeOtherContract.cs @@ -0,0 +1,14 @@ +using System; + +namespace Demo.Contracts +{ + public class SomeOtherContract + : IContract + { + public string Id { get; set; } + + public string CustomerId { get; set; } + + public DateTime ExpiryDate { get; set; } + } +} diff --git a/zurich_usergroup_2019/5_stitching/ContractSchema/SomeOtherContractType.cs b/zurich_usergroup_2019/5_stitching/ContractSchema/SomeOtherContractType.cs new file mode 100644 index 0000000..303d676 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/ContractSchema/SomeOtherContractType.cs @@ -0,0 +1,23 @@ +using HotChocolate.Types; + +namespace Demo.Contracts +{ + public class SomeOtherContractType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + + descriptor.Field(t => t.Id) + .Type>(); + + descriptor.Field(t => t.CustomerId) + .Ignore(); + + descriptor.Field(t => t.ExpiryDate) + .Type>(); + } + } +} diff --git a/zurich_usergroup_2019/5_stitching/ContractSchema/Startup.cs b/zurich_usergroup_2019/5_stitching/ContractSchema/Startup.cs new file mode 100644 index 0000000..71ea528 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/ContractSchema/Startup.cs @@ -0,0 +1,39 @@ +using HotChocolate; +using HotChocolate.AspNetCore; +using Demo.Contracts; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Demo.Contracts +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + + // Add GraphQL Services + services.AddGraphQL(Schema.Create(c => + { + c.RegisterQueryType(); + c.RegisterType(); + c.RegisterType(); + + c.UseGlobalObjectIdentifier(); + })); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(); + app.UsePlayground(); + } + } +} diff --git a/zurich_usergroup_2019/5_stitching/ContractSchema/contract.csproj b/zurich_usergroup_2019/5_stitching/ContractSchema/contract.csproj new file mode 100644 index 0000000..c36ea18 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/ContractSchema/contract.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp2.1 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + + + \ No newline at end of file diff --git a/zurich_usergroup_2019/5_stitching/CustomerSchema/Consultant.cs b/zurich_usergroup_2019/5_stitching/CustomerSchema/Consultant.cs new file mode 100644 index 0000000..903e6c5 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/CustomerSchema/Consultant.cs @@ -0,0 +1,9 @@ +namespace Demo.Customers +{ + public class Consultant + : ICustomerOrConsultant + { + public string Id { get; set; } + public string Name { get; set; } + } +} diff --git a/zurich_usergroup_2019/5_stitching/CustomerSchema/ConsultantType.cs b/zurich_usergroup_2019/5_stitching/CustomerSchema/ConsultantType.cs new file mode 100644 index 0000000..d3d3e94 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/CustomerSchema/ConsultantType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace Demo.Customers +{ + public class ConsultantType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Name).Type>(); + } + } +} diff --git a/zurich_usergroup_2019/5_stitching/CustomerSchema/Customer.cs b/zurich_usergroup_2019/5_stitching/CustomerSchema/Customer.cs new file mode 100644 index 0000000..a0c5abc --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/CustomerSchema/Customer.cs @@ -0,0 +1,10 @@ +namespace Demo.Customers +{ + public class Customer + : ICustomerOrConsultant + { + public string Id { get; set; } + public string Name { get; set; } + public string ConsultantId { get; set; } + } +} diff --git a/zurich_usergroup_2019/5_stitching/CustomerSchema/CustomerOrConsultantType.cs b/zurich_usergroup_2019/5_stitching/CustomerSchema/CustomerOrConsultantType.cs new file mode 100644 index 0000000..e6aed53 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/CustomerSchema/CustomerOrConsultantType.cs @@ -0,0 +1,15 @@ +using HotChocolate.Types; + +namespace Demo.Customers +{ + public class CustomerOrConsultantType + : UnionType + { + protected override void Configure(IUnionTypeDescriptor descriptor) + { + descriptor.Name("CustomerOrConsultant"); + descriptor.Type(); + descriptor.Type(); + } + } +} diff --git a/zurich_usergroup_2019/5_stitching/CustomerSchema/CustomerRepository.cs b/zurich_usergroup_2019/5_stitching/CustomerSchema/CustomerRepository.cs new file mode 100644 index 0000000..0b11ec7 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/CustomerSchema/CustomerRepository.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace Demo.Customers +{ + public class CustomerRepository + { + public List Customers { get; } = new List + { + new Customer + { + Id = "1", + Name = "Freddy Freeman", + ConsultantId = "1" + }, + new Customer + { + Id = "2", + Name = "Carol Danvers", + ConsultantId = "1" + }, + new Customer + { + Id = "3", + Name = "Walter Lawson", + ConsultantId = "2" + } + }; + + public List Consultants { get; } = new List + { + new Consultant + { + Id = "1", + Name = "Jordan Belfort", + }, + new Consultant + { + Id = "2", + Name = "Gordon Gekko", + } + }; + } +} diff --git a/zurich_usergroup_2019/5_stitching/CustomerSchema/CustomerResolver.cs b/zurich_usergroup_2019/5_stitching/CustomerSchema/CustomerResolver.cs new file mode 100644 index 0000000..f30784e --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/CustomerSchema/CustomerResolver.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq; +using HotChocolate; + +namespace Demo.Customers +{ + public class CustomerResolver + { + public Consultant GetConsultant( + Customer customer, + [Service]CustomerRepository repository) + { + if (customer.ConsultantId != null) + { + return repository.Consultants.FirstOrDefault( + t => t.Id.Equals(customer.ConsultantId)); + } + return null; + } + } +} diff --git a/zurich_usergroup_2019/5_stitching/CustomerSchema/CustomerType.cs b/zurich_usergroup_2019/5_stitching/CustomerSchema/CustomerType.cs new file mode 100644 index 0000000..79e2d17 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/CustomerSchema/CustomerType.cs @@ -0,0 +1,20 @@ +using HotChocolate.Types; + +namespace Demo.Customers +{ + public class CustomerType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Id).Type>(); + descriptor.Field(t => t.Name).Type>(); + descriptor.Field(t => t.ConsultantId).Ignore(); + + descriptor.Field( + t => t.GetConsultant(default, default)) + .Type(); + } + } +} diff --git a/zurich_usergroup_2019/5_stitching/CustomerSchema/ICustomerOrConsultant.cs b/zurich_usergroup_2019/5_stitching/CustomerSchema/ICustomerOrConsultant.cs new file mode 100644 index 0000000..17abcb0 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/CustomerSchema/ICustomerOrConsultant.cs @@ -0,0 +1,7 @@ +namespace Demo.Customers +{ + public interface ICustomerOrConsultant + { + + } +} diff --git a/zurich_usergroup_2019/5_stitching/CustomerSchema/Program.cs b/zurich_usergroup_2019/5_stitching/CustomerSchema/Program.cs new file mode 100644 index 0000000..15df5b0 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/CustomerSchema/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Demo.Customers +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseUrls("http://localhost:5050") + .UseStartup(); + } +} diff --git a/zurich_usergroup_2019/5_stitching/CustomerSchema/Query.cs b/zurich_usergroup_2019/5_stitching/CustomerSchema/Query.cs new file mode 100644 index 0000000..f4a25e9 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/CustomerSchema/Query.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HotChocolate.Types.Relay; + +namespace Demo.Customers +{ + public class Query + { + private readonly IdSerializer _idSerializer = new IdSerializer(); + private readonly CustomerRepository _repository; + + public Query(CustomerRepository repository) + { + _repository = repository + ?? throw new ArgumentNullException(nameof(repository)); + } + + public Customer GetCustomer(string id) + { + IdValue value = _idSerializer.Deserialize(id); + return _repository.Customers + .FirstOrDefault(t => t.Id.Equals(value.Value)); + } + + public IEnumerable GetCustomers() + { + return _repository.Customers; + } + + public Consultant GetConsultant(string id) + { + IdValue value = _idSerializer.Deserialize(id); + return _repository.Consultants + .FirstOrDefault(t => t.Id.Equals(value.Value)); + } + + public ICustomerOrConsultant GetCustomerOrConsultant(string id) + { + IdValue value = _idSerializer.Deserialize(id); + if (value.TypeName == "Consultant") + { + return GetConsultant(id); + } + return GetCustomer(id); + } + } +} diff --git a/zurich_usergroup_2019/5_stitching/CustomerSchema/QueryType.cs b/zurich_usergroup_2019/5_stitching/CustomerSchema/QueryType.cs new file mode 100644 index 0000000..183bae0 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/CustomerSchema/QueryType.cs @@ -0,0 +1,27 @@ +using HotChocolate.Types; + +namespace Demo.Customers +{ + public class QueryType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.GetCustomer(default)) + .Argument("id", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.GetCustomers()) + .Type>>>(); + + descriptor.Field(t => t.GetConsultant(default)) + .Argument("id", a => a.Type>()) + .Type(); + + descriptor.Field(t => t.GetCustomerOrConsultant(default)) + .Argument("id", a => a.Type>()) + .Type(); + } + } +} diff --git a/zurich_usergroup_2019/5_stitching/CustomerSchema/Startup.cs b/zurich_usergroup_2019/5_stitching/CustomerSchema/Startup.cs new file mode 100644 index 0000000..4bcace5 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/CustomerSchema/Startup.cs @@ -0,0 +1,35 @@ +using HotChocolate; +using HotChocolate.AspNetCore; +using Demo.Customers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Demo.Customers +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + + // Add GraphQL Services + services.AddGraphQL(Schema.Create(c => + { + c.RegisterQueryType(); + c.UseGlobalObjectIdentifier(); + })); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(); + app.UsePlayground(); + } + } +} diff --git a/zurich_usergroup_2019/5_stitching/CustomerSchema/customer.csproj b/zurich_usergroup_2019/5_stitching/CustomerSchema/customer.csproj new file mode 100644 index 0000000..c36ea18 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/CustomerSchema/customer.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp2.1 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + + + \ No newline at end of file diff --git a/zurich_usergroup_2019/5_stitching/Gateway/.vscode/launch.json b/zurich_usergroup_2019/5_stitching/Gateway/.vscode/launch.json new file mode 100644 index 0000000..2ed183d --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/Gateway/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.1/stitched.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart", + "launchBrowser": { + "enabled": true, + "args": "${auto-detect-url}", + "windows": { + "command": "cmd.exe", + "args": "/C start ${auto-detect-url}" + }, + "osx": { + "command": "open" + }, + "linux": { + "command": "xdg-open" + } + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/zurich_usergroup_2019/5_stitching/Gateway/.vscode/tasks.json b/zurich_usergroup_2019/5_stitching/Gateway/.vscode/tasks.json new file mode 100644 index 0000000..a914fef --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/Gateway/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet build", + "type": "shell", + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/zurich_usergroup_2019/5_stitching/Gateway/Extensions.graphql b/zurich_usergroup_2019/5_stitching/Gateway/Extensions.graphql new file mode 100644 index 0000000..97b9272 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/Gateway/Extensions.graphql @@ -0,0 +1,12 @@ +type Query { + me: Customer + @delegate( + schema: "customer" + path: "customer(id:$contextData:currentUserId)" + ) +} + +extend type Customer { + contracts: [Contract!] + @delegate(schema: "contract", path: "contracts(customerId:$fields:id)") +} diff --git a/zurich_usergroup_2019/5_stitching/Gateway/Program.cs b/zurich_usergroup_2019/5_stitching/Gateway/Program.cs new file mode 100644 index 0000000..f629620 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/Gateway/Program.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace Demo.Stitching +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/zurich_usergroup_2019/5_stitching/Gateway/SomeOtherContractExtension.cs b/zurich_usergroup_2019/5_stitching/Gateway/SomeOtherContractExtension.cs new file mode 100644 index 0000000..2fe6fa7 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/Gateway/SomeOtherContractExtension.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using HotChocolate; +using HotChocolate.Stitching; +using HotChocolate.Types; + +namespace Demo.Stitching +{ + public class SomeOtherContractExtension + : ObjectTypeExtension + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name("SomeOtherContract"); + descriptor.Field("expiresInDays") + .Type>() + .Directive(new ComputedDirective { DependantOn = new NameString[] { "expiryDate" } }) + .Resolver(context => + { + var obj = context.Parent>(); + var serializedExpiryDate = obj["expiryDate"]; + var dateType = (ISerializableType)context.ObjectType.Fields["expiryDate"].Type; + var offset = (DateTimeOffset)dateType.Deserialize(serializedExpiryDate); + return offset.DateTime.Subtract(DateTime.UtcNow).Days; + }); + } + } + +} diff --git a/zurich_usergroup_2019/5_stitching/Gateway/Startup.cs b/zurich_usergroup_2019/5_stitching/Gateway/Startup.cs new file mode 100644 index 0000000..8a6d605 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/Gateway/Startup.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.AspNetCore; +using HotChocolate.Stitching; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Demo.Stitching +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + // Setup the clients that shall be used to access the remote endpoints. + services.AddHttpClient("customer", (sp, client) => + { + // in order to pass on the token or any other headers to the backend schema use the IHttpContextAccessor + HttpContext context = sp.GetRequiredService().HttpContext; + client.BaseAddress = new Uri("http://127.0.0.1:5050"); + }); + + services.AddHttpClient("contract", (sp, client) => + { + // in order to pass on the token or any other headers to the backend schema use the IHttpContextAccessor + HttpContext context = sp.GetRequiredService().HttpContext; + client.BaseAddress = new Uri("http://127.0.0.1:5051"); + }); + + services.AddHttpContextAccessor(); + + services.AddQueryRequestInterceptor((context, builder, cancellationToken) => + { + builder.AddProperty("currentUserId", "Q3VzdG9tZXIKZDE="); + return Task.CompletedTask; + }); + + services.AddStitchedSchema(builder => builder + .AddSchemaFromHttp("customer") + .AddSchemaFromHttp("contract") + .AddExtensionsFromFile("./Extensions.graphql") + .IgnoreRootTypes() + .RenameType("LifeInsuranceContract", "LifeInsurance") + .AddSchemaConfiguration(c => + { + c.RegisterType(); + })); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseGraphQL(new QueryMiddlewareOptions { EnableSubscriptions = false }); + app.UsePlayground(); + } + } +} diff --git a/zurich_usergroup_2019/5_stitching/Gateway/stitched.csproj b/zurich_usergroup_2019/5_stitching/Gateway/stitched.csproj new file mode 100644 index 0000000..b8aef24 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/Gateway/stitched.csproj @@ -0,0 +1,35 @@ + + + + netcoreapp2.1 + 7.2 + + + + Full + true + + + + pdbonly + true + + + + + + + + + + + + + + + + Always + + + + diff --git a/zurich_usergroup_2019/5_stitching/README.md b/zurich_usergroup_2019/5_stitching/README.md new file mode 100644 index 0000000..d807a0c --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/README.md @@ -0,0 +1,20 @@ +# Schema Stitching Example + +This example shows how you can implement as stitched schema with Hot Chocolate. + +This example consists of the following projects: + +- CustomerSchema + The customer schema contains a GraphQL server that serves up a schema around a customer entity. + +- ContractSchema + The contract schema contains a GraphQL server that serves up a schema that provides insurance contract entities that can be assoicated with customers. + +- Stitching + The stitching project contains a GraphQL server that stitches the former mentiond GraphQL schemas together. + +1. Start the customer and contract servers with `dotnet run` +2. When the former servers are running start the stitching server with `dotnet run` +3. Head over to `http://127.0.0.1/playground` and test out some queries. + +[Hot Chocolate Documentation](https://hotchocolate.io) diff --git a/zurich_usergroup_2019/5_stitching/Stitching.sln b/zurich_usergroup_2019/5_stitching/Stitching.sln new file mode 100644 index 0000000..ca605b6 --- /dev/null +++ b/zurich_usergroup_2019/5_stitching/Stitching.sln @@ -0,0 +1,62 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "contract", "ContractSchema\contract.csproj", "{09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "customer", "CustomerSchema\customer.csproj", "{C538BC50-D306-4AE3-8FE4-38481C27427D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "stitched", "Gateway\stitched.csproj", "{E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|x64.ActiveCfg = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|x64.Build.0 = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|x86.ActiveCfg = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Debug|x86.Build.0 = Debug|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|Any CPU.Build.0 = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|x64.ActiveCfg = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|x64.Build.0 = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|x86.ActiveCfg = Release|Any CPU + {09BE2A34-A940-4A8C-A8EE-DDE2FCD60F20}.Release|x86.Build.0 = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|x64.ActiveCfg = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|x64.Build.0 = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Debug|x86.Build.0 = Debug|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|Any CPU.Build.0 = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|x64.ActiveCfg = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|x64.Build.0 = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|x86.ActiveCfg = Release|Any CPU + {C538BC50-D306-4AE3-8FE4-38481C27427D}.Release|x86.Build.0 = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|x64.ActiveCfg = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|x64.Build.0 = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|x86.ActiveCfg = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Debug|x86.Build.0 = Debug|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|Any CPU.Build.0 = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|x64.ActiveCfg = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|x64.Build.0 = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|x86.ActiveCfg = Release|Any CPU + {E03E32F0-B8A0-48E6-A8F5-F9631453CE1F}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/zurich_usergroup_2019/6_testing/.vscode/launch.json b/zurich_usergroup_2019/6_testing/.vscode/launch.json new file mode 100644 index 0000000..cc09e8b --- /dev/null +++ b/zurich_usergroup_2019/6_testing/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/netcoreapp3.0/Testing.dll", + "args": [], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/zurich_usergroup_2019/6_testing/.vscode/tasks.json b/zurich_usergroup_2019/6_testing/.vscode/tasks.json new file mode 100644 index 0000000..31c32bd --- /dev/null +++ b/zurich_usergroup_2019/6_testing/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/zurich_usergroup_2019/6_testing/Testing.csproj b/zurich_usergroup_2019/6_testing/Testing.csproj new file mode 100644 index 0000000..43d1d5f --- /dev/null +++ b/zurich_usergroup_2019/6_testing/Testing.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.0 + _9_testing + + false + + + + + + + + + + + + diff --git a/zurich_usergroup_2019/6_testing/UnitTest1.cs b/zurich_usergroup_2019/6_testing/UnitTest1.cs new file mode 100644 index 0000000..094658a --- /dev/null +++ b/zurich_usergroup_2019/6_testing/UnitTest1.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Execution; +using HotChocolate.Types; +using Snapshooter.Xunit; +using Xunit; + +namespace Testing +{ + public class MyTests + { + [Fact] + public void Schema_Snapshot() + { + // arrange + // act + ISchema schema = SchemaBuilder.New() + .AddQueryType(d => d + .Name("Query") + .Field("foo") + .Resolver("bar")) + .Create(); + + // assert + schema.ToString().MatchSnapshot(); + } + + [Fact] + public async Task Schema_Integration_Test() + { + // arrange + ISchema schema = SchemaBuilder.New() + .AddQueryType(d => + { + d.Name("Query"); + d.Field("foo").Resolver("bar"); + d.Field("baz").Resolver(DateTimeOffset.UtcNow); + }) + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + // act + IExecutionResult result = await executor.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ foo baz }") + .Create()); + + // assert + result.MatchSnapshot(matchOptions => matchOptions.IgnoreField("Data.baz")); + } + } +} diff --git a/zurich_usergroup_2019/GraphQL_Meetup.pptx b/zurich_usergroup_2019/GraphQL_Meetup.pptx new file mode 100644 index 0000000..f13a258 Binary files /dev/null and b/zurich_usergroup_2019/GraphQL_Meetup.pptx differ