diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f31a947 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# https://editorconfig.org/ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.cs] +indent_size = 4 diff --git a/.gitignore b/.gitignore index ea8ed55..bf31465 100644 --- a/.gitignore +++ b/.gitignore @@ -1,310 +1,5 @@ -## 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 - -# Ignore website as we do not have it in 11 -website/ - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# 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 - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ -**/Properties/launchSettings.json - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.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 - -# 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 - -# 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 -# TODO: 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 -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/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 - -# 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 - -# 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 - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ - -# 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 +__MISMATCH__/ .idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -lcov.info -coverage.json -.DS_Store - -BenchmarkDotNet.Artifacts -.sonarqube/ -.testresults - -# Build related -tools/** -!tools/packages.config -!tools/Build.sln -!tools/Build.Core.sln - -testoutput/ -artifacts/ -__mismatch__ -conferences.db \ No newline at end of file +*.DotSettings.user +bin/ +obj/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..ed37d37 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "davidanson.vscode-markdownlint", + "EditorConfig.EditorConfig", + "streetsidesoftware.code-spell-checker" + ] +} diff --git a/README.md b/README.md index 8ed4bcd..6ccb315 100644 --- a/README.md +++ b/README.md @@ -4,35 +4,34 @@ If you want to browse the GraphQL server head over [here](http://workshop.chilli ## Prerequisites -For this workshop we need a couple of prerequisites. First, we need the [.NET SDK 5.0](https://dotnet.microsoft.com/download/dotnet/5.0). +For this workshop we need a couple of prerequisites. First, we need the [.NET SDK 8.0](https://dotnet.microsoft.com/download/dotnet/8.0). -Then we need some IDE/Editor in order to do some proper C# coding, you can use [VSCode](https://code.visualstudio.com/) or if you have already on your system Visual Studio or JetBrains Rider. +Then we need an IDE/editor in order to do some proper C# coding, you can use [VSCode](https://code.visualstudio.com/) or if you already have it on your system, Visual Studio or JetBrains Rider. -Last but not least we will use our GraphQL IDE [Banana Cake Pop](https://chillicream.com/docs/bananacakepop). +Last but not least we will use our GraphQL IDE [Nitro](https://get-nitro.chillicream.com). > Note: When installing Visual Studio you only need to install the `ASP.NET and web development` workload. ## What you'll be building -In this workshop, you'll learn by building a full-featured GraphQL Server with ASP.NET Core and Hot Chocolate from scratch. We'll start from File/New and build up a full-featured GraphQL server with custom middleware, filters, subscription and relay support. +In this workshop, you'll learn by building a full-featured GraphQL Server with ASP.NET Core and Hot Chocolate from scratch. We'll start from File/New and build up a full-featured GraphQL server with custom middleware, filters, subscriptions, and Relay support. **Database Schema**: -![Database Schema Diagram](docs/images/21-conference-planner-db-diagram.png) +![Database Schema Diagram](docs/images/21-conference-planner-db-diagram.webp) **GraphQL Schema**: -The GraphQL schema can be found [here](code/complete/schema.graphql). +The GraphQL schema can be found [here](code/session-7/GraphQL.Tests/__snapshots__/SchemaTests.SchemaChanged.graphql). ## Sessions -| Session | Topics | -| ----- | ---- | -| [Session #1](docs/1-creating-a-graphql-server-project.md) | Building a basic GraphQL server API. | -| [Session #2](docs/2-controlling-nullability.md) | Controlling nullability. | -| [Session #3](docs/3-understanding-dataLoader.md) | Understanding GraphQL query execution and DataLoader. | -| [Session #4](docs/4-schema-design.md) | GraphQL schema design approaches. | -| [Session #5](docs/5-understanding-middleware.md) | Understanding middleware. | -| [Session #6](docs/6-adding-complex-filter-capabilities.md) | Adding complex filter capabilities. | -| [Session #7](docs/7-subscriptions.md) | Adding real-time functionality with subscriptions. | -| [Session #8](docs/8-testing-the-graphql-server.md) | Testing the GraphQL server. | +| Session | Topics | +|------------------------------------------------------------|----------------------------------------------------| +| [Session #1](docs/1-creating-a-graphql-server-project.md) | Creating a new GraphQL server project. | +| [Session #2](docs/2-understanding-data-loader.md) | Understanding DataLoader. | +| [Session #3](docs/3-schema-design.md) | GraphQL schema design approaches. | +| [Session #4](docs/4-understanding-middleware.md) | Understanding middleware. | +| [Session #5](docs/5-adding-complex-filter-capabilities.md) | Adding complex filter capabilities. | +| [Session #6](docs/6-subscriptions.md) | Adding real-time functionality with subscriptions. | +| [Session #7](docs/7-testing-the-graphql-server.md) | Testing the GraphQL server. | diff --git a/code/GraphQLWorkshop.sln b/code/GraphQLWorkshop.sln new file mode 100644 index 0000000..838e801 --- /dev/null +++ b/code/GraphQLWorkshop.sln @@ -0,0 +1,88 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "session-1", "session-1", "{11786640-0CEE-4D1E-8729-9904FA10C0DF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL", "session-1\GraphQL\GraphQL.csproj", "{D29C7593-D878-40BC-953F-971420A03403}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "session-2", "session-2", "{7C8546F3-F99B-45A2-81A9-C3CB263748E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL", "session-2\GraphQL\GraphQL.csproj", "{DF4EA51B-471F-4B0C-9737-5C6D68484CE2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "session-3", "session-3", "{F67C3085-53AB-459B-936F-C46905CDCDEA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL", "session-3\GraphQL\GraphQL.csproj", "{E452B369-E54E-4BD1-B856-34CF3019D428}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "session-4", "session-4", "{3D651CB0-21E7-49CD-B19D-E9D75BBDA06D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL", "session-4\GraphQL\GraphQL.csproj", "{670B0591-4E59-4F20-B650-6B57A0BA9189}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "session-5", "session-5", "{E98E6210-25DA-4386-BBEF-D13635BF6849}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL", "session-5\GraphQL\GraphQL.csproj", "{FBCE7AE4-1A4A-4BA2-B6E4-DDBE77B8A8C3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "session-6", "session-6", "{D2283398-B9A6-4A19-869C-F70EA85BB7D8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL", "session-6\GraphQL\GraphQL.csproj", "{2D01FDB9-6476-4AA9-ADA7-889BF12F7623}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "session-7", "session-7", "{C08F3869-99AC-4EF6-AC25-CC03B8CA35BC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL", "session-7\GraphQL\GraphQL.csproj", "{B56B6099-B71D-4C5A-BF5B-EEED842E5A01}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Tests", "session-7\GraphQL.Tests\GraphQL.Tests.csproj", "{816B5BF4-4B80-4C78-A9A6-1567864FF732}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D29C7593-D878-40BC-953F-971420A03403}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D29C7593-D878-40BC-953F-971420A03403}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D29C7593-D878-40BC-953F-971420A03403}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D29C7593-D878-40BC-953F-971420A03403}.Release|Any CPU.Build.0 = Release|Any CPU + {DF4EA51B-471F-4B0C-9737-5C6D68484CE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF4EA51B-471F-4B0C-9737-5C6D68484CE2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF4EA51B-471F-4B0C-9737-5C6D68484CE2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF4EA51B-471F-4B0C-9737-5C6D68484CE2}.Release|Any CPU.Build.0 = Release|Any CPU + {E452B369-E54E-4BD1-B856-34CF3019D428}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E452B369-E54E-4BD1-B856-34CF3019D428}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E452B369-E54E-4BD1-B856-34CF3019D428}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E452B369-E54E-4BD1-B856-34CF3019D428}.Release|Any CPU.Build.0 = Release|Any CPU + {670B0591-4E59-4F20-B650-6B57A0BA9189}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {670B0591-4E59-4F20-B650-6B57A0BA9189}.Debug|Any CPU.Build.0 = Debug|Any CPU + {670B0591-4E59-4F20-B650-6B57A0BA9189}.Release|Any CPU.ActiveCfg = Release|Any CPU + {670B0591-4E59-4F20-B650-6B57A0BA9189}.Release|Any CPU.Build.0 = Release|Any CPU + {FBCE7AE4-1A4A-4BA2-B6E4-DDBE77B8A8C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBCE7AE4-1A4A-4BA2-B6E4-DDBE77B8A8C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBCE7AE4-1A4A-4BA2-B6E4-DDBE77B8A8C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBCE7AE4-1A4A-4BA2-B6E4-DDBE77B8A8C3}.Release|Any CPU.Build.0 = Release|Any CPU + {2D01FDB9-6476-4AA9-ADA7-889BF12F7623}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D01FDB9-6476-4AA9-ADA7-889BF12F7623}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D01FDB9-6476-4AA9-ADA7-889BF12F7623}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D01FDB9-6476-4AA9-ADA7-889BF12F7623}.Release|Any CPU.Build.0 = Release|Any CPU + {B56B6099-B71D-4C5A-BF5B-EEED842E5A01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B56B6099-B71D-4C5A-BF5B-EEED842E5A01}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B56B6099-B71D-4C5A-BF5B-EEED842E5A01}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B56B6099-B71D-4C5A-BF5B-EEED842E5A01}.Release|Any CPU.Build.0 = Release|Any CPU + {816B5BF4-4B80-4C78-A9A6-1567864FF732}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {816B5BF4-4B80-4C78-A9A6-1567864FF732}.Debug|Any CPU.Build.0 = Debug|Any CPU + {816B5BF4-4B80-4C78-A9A6-1567864FF732}.Release|Any CPU.ActiveCfg = Release|Any CPU + {816B5BF4-4B80-4C78-A9A6-1567864FF732}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D29C7593-D878-40BC-953F-971420A03403} = {11786640-0CEE-4D1E-8729-9904FA10C0DF} + {DF4EA51B-471F-4B0C-9737-5C6D68484CE2} = {7C8546F3-F99B-45A2-81A9-C3CB263748E8} + {E452B369-E54E-4BD1-B856-34CF3019D428} = {F67C3085-53AB-459B-936F-C46905CDCDEA} + {670B0591-4E59-4F20-B650-6B57A0BA9189} = {3D651CB0-21E7-49CD-B19D-E9D75BBDA06D} + {FBCE7AE4-1A4A-4BA2-B6E4-DDBE77B8A8C3} = {E98E6210-25DA-4386-BBEF-D13635BF6849} + {2D01FDB9-6476-4AA9-ADA7-889BF12F7623} = {D2283398-B9A6-4A19-869C-F70EA85BB7D8} + {B56B6099-B71D-4C5A-BF5B-EEED842E5A01} = {C08F3869-99AC-4EF6-AC25-CC03B8CA35BC} + {816B5BF4-4B80-4C78-A9A6-1567864FF732} = {C08F3869-99AC-4EF6-AC25-CC03B8CA35BC} + EndGlobalSection +EndGlobal diff --git a/code/complete/.config/dotnet-tools.json b/code/complete/.config/dotnet-tools.json deleted file mode 100644 index 82e7e22..0000000 --- a/code/complete/.config/dotnet-tools.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "dotnet-ef": { - "version": "6.0.2", - "commands": [ - "dotnet-ef" - ] - } - } -} \ No newline at end of file diff --git a/code/complete/.vscode/launch.json b/code/complete/.vscode/launch.json deleted file mode 100644 index ee7bdcb..0000000 --- a/code/complete/.vscode/launch.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - // 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}/GraphQL/bin/Debug/net6.0/GraphQL.dll", - "args": [], - "cwd": "${workspaceFolder}/GraphQL", - "stopAtEntry": false, - "serverReadyAction": { - "action": "openExternally", - "pattern": "\\bNow 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/code/complete/.vscode/tasks.json b/code/complete/.vscode/tasks.json deleted file mode 100644 index 31c32bd..0000000 --- a/code/complete/.vscode/tasks.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - // 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/code/complete/ConferencePlanner.sln b/code/complete/ConferencePlanner.sln deleted file mode 100644 index 3aff6ae..0000000 --- a/code/complete/ConferencePlanner.sln +++ /dev/null @@ -1,48 +0,0 @@ - -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}") = "GraphQL", "GraphQL\GraphQL.csproj", "{9CC0FB4B-0FB8-430F-9B19-2D3B9AFF64C5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Tests", "GraphQL.Tests\GraphQL.Tests.csproj", "{BFD8F041-E59D-47B3-B48D-8ADB237827A7}" -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 - {9CC0FB4B-0FB8-430F-9B19-2D3B9AFF64C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9CC0FB4B-0FB8-430F-9B19-2D3B9AFF64C5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9CC0FB4B-0FB8-430F-9B19-2D3B9AFF64C5}.Debug|x64.ActiveCfg = Debug|Any CPU - {9CC0FB4B-0FB8-430F-9B19-2D3B9AFF64C5}.Debug|x64.Build.0 = Debug|Any CPU - {9CC0FB4B-0FB8-430F-9B19-2D3B9AFF64C5}.Debug|x86.ActiveCfg = Debug|Any CPU - {9CC0FB4B-0FB8-430F-9B19-2D3B9AFF64C5}.Debug|x86.Build.0 = Debug|Any CPU - {9CC0FB4B-0FB8-430F-9B19-2D3B9AFF64C5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9CC0FB4B-0FB8-430F-9B19-2D3B9AFF64C5}.Release|Any CPU.Build.0 = Release|Any CPU - {9CC0FB4B-0FB8-430F-9B19-2D3B9AFF64C5}.Release|x64.ActiveCfg = Release|Any CPU - {9CC0FB4B-0FB8-430F-9B19-2D3B9AFF64C5}.Release|x64.Build.0 = Release|Any CPU - {9CC0FB4B-0FB8-430F-9B19-2D3B9AFF64C5}.Release|x86.ActiveCfg = Release|Any CPU - {9CC0FB4B-0FB8-430F-9B19-2D3B9AFF64C5}.Release|x86.Build.0 = Release|Any CPU - {BFD8F041-E59D-47B3-B48D-8ADB237827A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BFD8F041-E59D-47B3-B48D-8ADB237827A7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BFD8F041-E59D-47B3-B48D-8ADB237827A7}.Debug|x64.ActiveCfg = Debug|Any CPU - {BFD8F041-E59D-47B3-B48D-8ADB237827A7}.Debug|x64.Build.0 = Debug|Any CPU - {BFD8F041-E59D-47B3-B48D-8ADB237827A7}.Debug|x86.ActiveCfg = Debug|Any CPU - {BFD8F041-E59D-47B3-B48D-8ADB237827A7}.Debug|x86.Build.0 = Debug|Any CPU - {BFD8F041-E59D-47B3-B48D-8ADB237827A7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BFD8F041-E59D-47B3-B48D-8ADB237827A7}.Release|Any CPU.Build.0 = Release|Any CPU - {BFD8F041-E59D-47B3-B48D-8ADB237827A7}.Release|x64.ActiveCfg = Release|Any CPU - {BFD8F041-E59D-47B3-B48D-8ADB237827A7}.Release|x64.Build.0 = Release|Any CPU - {BFD8F041-E59D-47B3-B48D-8ADB237827A7}.Release|x86.ActiveCfg = Release|Any CPU - {BFD8F041-E59D-47B3-B48D-8ADB237827A7}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/code/complete/GraphQL.Tests/AttendeeTests.txt b/code/complete/GraphQL.Tests/AttendeeTests.txt deleted file mode 100644 index 4f04801..0000000 --- a/code/complete/GraphQL.Tests/AttendeeTests.txt +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL; -using ConferencePlanner.GraphQL.Attendees; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.Sessions; -using ConferencePlanner.GraphQL.Speakers; -using ConferencePlanner.GraphQL.Tracks; -using ConferencePlanner.GraphQL.Types; -using HotChocolate; -using HotChocolate.Execution; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Snapshooter.Xunit; -using Xunit; - -namespace GraphQL.Tests -{ - public class AttendeeTests - { - [Fact] - public async Task Attendee_Schema_Changed() - { - ISchema schema = - await new ServiceCollection() - .AddDbContextPool( - options => options.UseInMemoryDatabase("Data Source=conferences.db")) - .AddGraphQL() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport() - .BuildSchemaAsync(); - - schema.Print().MatchSnapshot(); - } - - [Fact] - public async Task RegisterAttendee() - { - // arrange - IServiceProvider services = new ServiceCollection() - .AddDbContextPool( - options => options.UseInMemoryDatabase("Data Source=conferences.db")) - .AddGraphQL() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - // .EnableRelaySupport() - .Services - .BuildServiceProvider(); - - // act - IExecutionResult result = await services.ExecuteRequestAsync( - QueryRequestBuilder.New() - .SetQuery(@" - mutation RegisterAttendee { - registerAttendee( - input: { - emailAddress: ""michael@chillicream.com"" - firstName: ""michael"" - lastName: ""staib"" - userName: ""michael3"" - }) - { - attendee { - id - } - } - }") - .Create()); - - // assert - result.MatchSnapshot(); - } - } -} diff --git a/code/complete/GraphQL.Tests/GraphQL.Tests.csproj b/code/complete/GraphQL.Tests/GraphQL.Tests.csproj deleted file mode 100644 index f02bbef..0000000 --- a/code/complete/GraphQL.Tests/GraphQL.Tests.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - net6.0 - - false - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - diff --git a/code/complete/GraphQL.Tests/__snapshots__/AttendeeTests.Attendee_Schema_Changed.snap b/code/complete/GraphQL.Tests/__snapshots__/AttendeeTests.Attendee_Schema_Changed.snap deleted file mode 100644 index b6b8b90..0000000 --- a/code/complete/GraphQL.Tests/__snapshots__/AttendeeTests.Attendee_Schema_Changed.snap +++ /dev/null @@ -1,157 +0,0 @@ -schema { - query: Query - mutation: Mutation -} - -"The node interface is implemented by entities that have a global unique identifier." -interface Node { - id: ID! -} - -type Attendee implements Node { - id: ID! - sessions: [Session] - firstName: String! - lastName: String! - userName: String! - emailAddress: String -} - -"A connection to a list of items." -type AttendeeConnection { - "Information to aid in pagination." - pageInfo: PageInfo! - "A list of edges." - edges: [AttendeeEdge!] - "A flattened list of the nodes." - nodes: [Attendee!] -} - -"An edge in a connection." -type AttendeeEdge { - "A cursor for use in pagination." - cursor: String! - "The item at the end of the edge." - node: Attendee! -} - -type CheckInAttendeePayload { - session: Session - attendee: Attendee - errors: [UserError!] -} - -type Mutation { - registerAttendee(input: RegisterAttendeeInput!): RegisterAttendeePayload! - checkInAttendee(input: CheckInAttendeeInput!): CheckInAttendeePayload! -} - -"Information about pagination in a connection." -type PageInfo { - "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 - "When paginating forwards, the cursor to continue." - endCursor: String -} - -type Query { - node(id: ID!): Node - attendees(first: Int after: String last: Int before: String): AttendeeConnection - attendeeById(id: ID!): Attendee! - attendeesById(ids: [ID!]!): [Attendee!]! -} - -type RegisterAttendeePayload { - attendee: Attendee - errors: [UserError!] -} - -type Session implements Node { - id: ID! - speakers: [Speaker] - attendees: [Attendee] - track: Track - trackId: ID - title: String! - abstract: String - startTime: DateTime - endTime: DateTime - duration: TimeSpan! -} - -"A connection to a list of items." -type SessionConnection { - "Information to aid in pagination." - pageInfo: PageInfo! - "A list of edges." - edges: [SessionEdge!] - "A flattened list of the nodes." - nodes: [Session!] -} - -"An edge in a connection." -type SessionEdge { - "A cursor for use in pagination." - cursor: String! - "The item at the end of the edge." - node: Session! -} - -type Speaker implements Node { - id: ID! - sessions: [Session] - name: String! - bio: String - webSite: String -} - -type Track implements Node { - id: ID! - sessions(first: Int after: String last: Int before: String): SessionConnection - name: String! -} - -type UserError { - message: String! - code: String! -} - -input CheckInAttendeeInput { - sessionId: ID! - attendeeId: ID! -} - -input RegisterAttendeeInput { - firstName: String! - lastName: String! - userName: String! - emailAddress: String! -} - -"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`." -directive @defer("If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String "Deferred when true." if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT - -"The @deprecated directive is used within the type system definition language to indicate deprecated portions of a GraphQL service’s schema,such as deprecated fields on a type or deprecated enum values." -directive @deprecated("Deprecations include a reason for why it is deprecated, which is formatted using Markdown syntax (as specified by CommonMark)." reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE - -"Directs the executor to include this field or fragment only when the `if` argument is true." -directive @include("Included when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - -"Directs the executor to skip this field or fragment when the `if` argument is true." -directive @skip("Skipped when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - -"The `@specifiedBy` directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar definitions." -directive @specifiedBy("The specifiedBy URL points to a human-readable specification. This field will only read a result for scalar types." url: String!) on SCALAR - -"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." -directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! "Streamed when true." if: Boolean!) on FIELD - -"The `DateTime` scalar represents an ISO-8601 compliant date time type." -scalar DateTime @specifiedBy(url: "https:\/\/www.graphql-scalars.com\/date-time") - -"The `TimeSpan` scalar represents an ISO-8601 compliant duration type." -scalar TimeSpan diff --git a/code/complete/GraphQL.Tests/__snapshots__/AttendeeTests.RegisterAttendee.snap b/code/complete/GraphQL.Tests/__snapshots__/AttendeeTests.RegisterAttendee.snap deleted file mode 100644 index 6f21783..0000000 --- a/code/complete/GraphQL.Tests/__snapshots__/AttendeeTests.RegisterAttendee.snap +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Data": { - "registerAttendee": { - "attendee": { - "id": "QXR0ZW5kZWUKaTE=" - } - } - }, - "Extensions": {}, - "Errors": [], - "ContextData": {} -} diff --git a/code/complete/GraphQL/Attendees/AttendeeMutations.cs b/code/complete/GraphQL/Attendees/AttendeeMutations.cs deleted file mode 100644 index 19a232c..0000000 --- a/code/complete/GraphQL/Attendees/AttendeeMutations.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Subscriptions; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Attendees -{ - [ExtendObjectType(OperationTypeNames.Mutation)] - public class AttendeeMutations - { - [UseApplicationDbContext] - public async Task RegisterAttendeeAsync( - RegisterAttendeeInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - var attendee = new Attendee - { - FirstName = input.FirstName, - LastName = input.LastName, - UserName = input.UserName, - EmailAddress = input.EmailAddress - }; - - context.Attendees.Add(attendee); - - await context.SaveChangesAsync(cancellationToken); - - return new RegisterAttendeePayload(attendee); - } - - [UseApplicationDbContext] - public async Task CheckInAttendeeAsync( - CheckInAttendeeInput input, - [ScopedService] ApplicationDbContext context, - [Service] ITopicEventSender eventSender, - CancellationToken cancellationToken) - { - var attendee = await context.Attendees.FirstOrDefaultAsync( - t => t.Id == input.AttendeeId, cancellationToken); - - if (attendee is null) - { - return new CheckInAttendeePayload( - new UserError("Attendee not found.", "ATTENDEE_NOT_FOUND")); - } - - attendee.SessionsAttendees.Add( - new SessionAttendee - { - SessionId = input.SessionId - }); - - await context.SaveChangesAsync(cancellationToken); - - await eventSender.SendAsync( - "OnAttendeeCheckedIn_" + input.SessionId, - input.AttendeeId, - cancellationToken); - - return new CheckInAttendeePayload(attendee, input.SessionId); - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Attendees/AttendeeNode.cs b/code/complete/GraphQL/Attendees/AttendeeNode.cs deleted file mode 100644 index 8ee53c8..0000000 --- a/code/complete/GraphQL/Attendees/AttendeeNode.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; -using Microsoft.EntityFrameworkCore; - -namespace ConferencePlanner.GraphQL.Attendees -{ - [Node] - [ExtendObjectType] - public class AttendeeNode - { - public async Task> GetSessionsAsync( - [Parent] Attendee attendee, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Attendees - .Where(a => a.Id == attendee.Id) - .Include(a => a.SessionsAttendees) - .SelectMany(a => a.SessionsAttendees.Select(t => t.SessionId)) - .ToArrayAsync(cancellationToken); - - return await sessionById.LoadAsync(speakerIds, cancellationToken); - } - - [NodeResolver] - public static Task GetAttendeeAsync( - int id, - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) - => attendeeById.LoadAsync(id, cancellationToken); - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Attendees/AttendeePayloadBase.cs b/code/complete/GraphQL/Attendees/AttendeePayloadBase.cs deleted file mode 100644 index ac81204..0000000 --- a/code/complete/GraphQL/Attendees/AttendeePayloadBase.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Attendees -{ - public class AttendeePayloadBase : Payload - { - protected AttendeePayloadBase(Attendee attendee) - { - } - - protected AttendeePayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Attendee? Attendee { get; } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Attendees/AttendeeQueries.cs b/code/complete/GraphQL/Attendees/AttendeeQueries.cs deleted file mode 100644 index 2fc3f75..0000000 --- a/code/complete/GraphQL/Attendees/AttendeeQueries.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Attendees -{ - [ExtendObjectType(OperationTypeNames.Query)] - public class AttendeeQueries - { - /// - /// Gets all attendees of this conference. - /// - [UseApplicationDbContext] - [UsePaging] - public IQueryable GetAttendees( - [ScopedService] ApplicationDbContext context) - => context.Attendees; - - /// - /// Gets an attendee by its identifier. - /// - /// The attendee identifier. - /// - /// - /// - public Task GetAttendeeByIdAsync( - [ID(nameof(Attendee))] int id, - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) - => attendeeById.LoadAsync(id, cancellationToken); - - public async Task> GetAttendeesByIdAsync( - [ID(nameof(Attendee))] int[] ids, - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) - => await attendeeById.LoadAsync(ids, cancellationToken); - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Attendees/AttendeeSubscriptions.cs b/code/complete/GraphQL/Attendees/AttendeeSubscriptions.cs deleted file mode 100644 index 62792a7..0000000 --- a/code/complete/GraphQL/Attendees/AttendeeSubscriptions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Execution; -using HotChocolate.Subscriptions; -using HotChocolate.Types; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Attendees -{ - [ExtendObjectType(OperationTypeNames.Subscription)] - public class AttendeeSubscriptions - { - [Subscribe(With = nameof(SubscribeToOnAttendeeCheckedInAsync))] - public SessionAttendeeCheckIn OnAttendeeCheckedIn( - [ID(nameof(Session))] int sessionId, - [EventMessage] int attendeeId) - => new(attendeeId, sessionId); - - public async ValueTask> SubscribeToOnAttendeeCheckedInAsync( - int sessionId, - [Service] ITopicEventReceiver eventReceiver, - CancellationToken cancellationToken) - => await eventReceiver.SubscribeAsync( - "OnAttendeeCheckedIn_" + sessionId, cancellationToken); - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Attendees/CheckInAttendeeInput.cs b/code/complete/GraphQL/Attendees/CheckInAttendeeInput.cs deleted file mode 100644 index a3dded1..0000000 --- a/code/complete/GraphQL/Attendees/CheckInAttendeeInput.cs +++ /dev/null @@ -1,11 +0,0 @@ -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Attendees -{ - public record CheckInAttendeeInput( - [property: ID(nameof(Session))] - int SessionId, - [property: ID(nameof(Attendee))] - int AttendeeId); -} \ No newline at end of file diff --git a/code/complete/GraphQL/Attendees/CheckInAttendeePayload.cs b/code/complete/GraphQL/Attendees/CheckInAttendeePayload.cs deleted file mode 100644 index 0d79369..0000000 --- a/code/complete/GraphQL/Attendees/CheckInAttendeePayload.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; - -namespace ConferencePlanner.GraphQL.Attendees -{ - public class CheckInAttendeePayload : AttendeePayloadBase - { - private readonly int? _sessionId; - - public CheckInAttendeePayload(Attendee attendee, int sessionId) - : base(attendee) - { - _sessionId = sessionId; - } - - public CheckInAttendeePayload(UserError error) - : base(new[] { error }) - { - } - - public async Task GetSessionAsync( - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - if (_sessionId.HasValue) - { - return await sessionById.LoadAsync(_sessionId.Value, cancellationToken); - } - - return null; - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Attendees/RegisterAttendeeInput.cs b/code/complete/GraphQL/Attendees/RegisterAttendeeInput.cs deleted file mode 100644 index 1710f0b..0000000 --- a/code/complete/GraphQL/Attendees/RegisterAttendeeInput.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ConferencePlanner.GraphQL.Attendees -{ - public record RegisterAttendeeInput( - string FirstName, - string LastName, - string UserName, - string EmailAddress); -} \ No newline at end of file diff --git a/code/complete/GraphQL/Attendees/RegisterAttendeePayload.cs b/code/complete/GraphQL/Attendees/RegisterAttendeePayload.cs deleted file mode 100644 index a79e99a..0000000 --- a/code/complete/GraphQL/Attendees/RegisterAttendeePayload.cs +++ /dev/null @@ -1,18 +0,0 @@ -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Attendees -{ - public class RegisterAttendeePayload : AttendeePayloadBase - { - public RegisterAttendeePayload(Attendee attendee) - : base(attendee) - { - } - - public RegisterAttendeePayload(UserError error) - : base(new[] { error }) - { - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Attendees/SessionAttendeeCheckIn.cs b/code/complete/GraphQL/Attendees/SessionAttendeeCheckIn.cs deleted file mode 100644 index 3b2b38b..0000000 --- a/code/complete/GraphQL/Attendees/SessionAttendeeCheckIn.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Attendees -{ - public class SessionAttendeeCheckIn - { - public SessionAttendeeCheckIn(int attendeeId, int sessionId) - { - AttendeeId = attendeeId; - SessionId = sessionId; - } - - [ID(nameof(Attendee))] - public int AttendeeId { get; } - - [ID(nameof(Session))] - public int SessionId { get; } - - [UseApplicationDbContext] - public async Task CheckInCountAsync( - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - => await context.Sessions - .Where(session => session.Id == SessionId) - .SelectMany(session => session.SessionAttendees) - .CountAsync(cancellationToken); - - public Task GetAttendeeAsync( - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) - => attendeeById.LoadAsync(AttendeeId, cancellationToken); - - public Task GetSessionAsync( - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - => sessionById.LoadAsync(AttendeeId, cancellationToken); - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Common/Payload.cs b/code/complete/GraphQL/Common/Payload.cs deleted file mode 100644 index e9d2839..0000000 --- a/code/complete/GraphQL/Common/Payload.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace ConferencePlanner.GraphQL.Common -{ - public abstract class Payload - { - protected Payload(IReadOnlyList? errors = null) - { - Errors = errors; - } - - public IReadOnlyList? Errors { get; } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Common/UserError.cs b/code/complete/GraphQL/Common/UserError.cs deleted file mode 100644 index c94c3fc..0000000 --- a/code/complete/GraphQL/Common/UserError.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace ConferencePlanner.GraphQL.Common -{ - public record UserError(string Message, string Code); -} \ No newline at end of file diff --git a/code/complete/GraphQL/Data/ApplicationDbContext.cs b/code/complete/GraphQL/Data/ApplicationDbContext.cs deleted file mode 100644 index 80e0abe..0000000 --- a/code/complete/GraphQL/Data/ApplicationDbContext.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace ConferencePlanner.GraphQL.Data -{ - public class ApplicationDbContext : DbContext - { - public ApplicationDbContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasIndex(a => a.UserName) - .IsUnique(); - - // Many-to-many: Session <-> Attendee - modelBuilder - .Entity() - .HasKey(ca => new { ca.SessionId, ca.AttendeeId }); - - // Many-to-many: Speaker <-> Session - modelBuilder - .Entity() - .HasKey(ss => new { ss.SessionId, ss.SpeakerId }); - } - - public DbSet Sessions { get; set; } = default!; - - public DbSet Tracks { get; set; } = default!; - - public DbSet Speakers { get; set; } = default!; - - public DbSet Attendees { get; set; } = default!; - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Data/Attendee.cs b/code/complete/GraphQL/Data/Attendee.cs deleted file mode 100644 index 580e503..0000000 --- a/code/complete/GraphQL/Data/Attendee.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace ConferencePlanner.GraphQL.Data -{ - public class Attendee - { - public int Id { get; set; } - - [Required] - [StringLength(200)] - public string? FirstName { get; set; } - - [Required] - [StringLength(200)] - public string? LastName { get; set; } - - [Required] - [StringLength(200)] - public string? UserName { get; set; } - - [StringLength(256)] - public string? EmailAddress { get; set; } - - [StringLength(256)] - public string? Country { get; set; } - - public ICollection SessionsAttendees { get; set; } = - new List(); - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Data/Session.cs b/code/complete/GraphQL/Data/Session.cs deleted file mode 100644 index e0dcd91..0000000 --- a/code/complete/GraphQL/Data/Session.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace ConferencePlanner.GraphQL.Data -{ - public class Session - { - public int Id { get; set; } - - [Required] - [StringLength(200)] - public string? Title { get; set; } - - [StringLength(4000)] - public string? Abstract { get; set; } - - public DateTimeOffset? StartTime { get; set; } - - public DateTimeOffset? EndTime { get; set; } - - // Bonus points to those who can figure out why this is written this way - public TimeSpan Duration => - EndTime?.Subtract(StartTime ?? EndTime ?? DateTimeOffset.MinValue) ?? - TimeSpan.Zero; - - public int? TrackId { get; set; } - - public ICollection SessionSpeakers { get; set; } = - new List(); - - public ICollection SessionAttendees { get; set; } = - new List(); - - public Track? Track { get; set; } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Data/SessionAttendee.cs b/code/complete/GraphQL/Data/SessionAttendee.cs deleted file mode 100644 index 089c71a..0000000 --- a/code/complete/GraphQL/Data/SessionAttendee.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace ConferencePlanner.GraphQL.Data -{ - public class SessionAttendee - { - public int SessionId { get; set; } - - public Session? Session { get; set; } - - public int AttendeeId { get; set; } - - public Attendee? Attendee { get; set; } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Data/SessionSpeaker.cs b/code/complete/GraphQL/Data/SessionSpeaker.cs deleted file mode 100644 index ed83e86..0000000 --- a/code/complete/GraphQL/Data/SessionSpeaker.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace ConferencePlanner.GraphQL.Data -{ - public class SessionSpeaker - { - public int SessionId { get; set; } - - public Session? Session { get; set; } - - public int SpeakerId { get; set; } - - public Speaker? Speaker { get; set; } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Data/Speaker.cs b/code/complete/GraphQL/Data/Speaker.cs deleted file mode 100644 index 999094c..0000000 --- a/code/complete/GraphQL/Data/Speaker.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace ConferencePlanner.GraphQL.Data -{ - public class Speaker - { - public int Id { get; set; } - - [Required] - [StringLength(200)] - public string? Name { get; set; } - - [StringLength(4000)] - public string? Bio { get; set; } - - [StringLength(1000)] - public string? WebSite { get; set; } - - public ICollection SessionSpeakers { get; set; } = - new List(); - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Data/Track.cs b/code/complete/GraphQL/Data/Track.cs deleted file mode 100644 index 0d41582..0000000 --- a/code/complete/GraphQL/Data/Track.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace ConferencePlanner.GraphQL.Data -{ - public class Track - { - public int Id { get; set; } - - [Required] - [StringLength(200)] - public string? Name { get; set; } - - public ICollection Sessions { get; set; } = - new List(); - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/DataLoader/AttendeeByIdDataLoader.cs b/code/complete/GraphQL/DataLoader/AttendeeByIdDataLoader.cs deleted file mode 100644 index e96eacf..0000000 --- a/code/complete/GraphQL/DataLoader/AttendeeByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class AttendeeByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public AttendeeByIdDataLoader( - IDbContextFactory dbContextFactory, - IBatchScheduler batchScheduler, - DataLoaderOptions options) - : base(batchScheduler, options) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Attendees - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/DataLoader/SessionByIdDataLoader.cs b/code/complete/GraphQL/DataLoader/SessionByIdDataLoader.cs deleted file mode 100644 index 12cda45..0000000 --- a/code/complete/GraphQL/DataLoader/SessionByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class SessionByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public SessionByIdDataLoader( - IDbContextFactory dbContextFactory, - IBatchScheduler batchScheduler, - DataLoaderOptions options) - : base(batchScheduler, options) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Sessions - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/DataLoader/SessionBySpeakerIdDataLoader.cs b/code/complete/GraphQL/DataLoader/SessionBySpeakerIdDataLoader.cs deleted file mode 100644 index b153d7a..0000000 --- a/code/complete/GraphQL/DataLoader/SessionBySpeakerIdDataLoader.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using Microsoft.EntityFrameworkCore; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class SessionBySpeakerIdDataLoader : GroupedDataLoader - { - private static readonly string _sessionCacheKey = GetCacheKeyType(); - private readonly IDbContextFactory _dbContextFactory; - - public SessionBySpeakerIdDataLoader( - IDbContextFactory dbContextFactory, - IBatchScheduler batchScheduler, - DataLoaderOptions options) - : base(batchScheduler, options) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadGroupedBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - List list = await dbContext.Speakers - .Where(s => keys.Contains(s.Id)) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers) - .Include(s => s.Session) - .ToListAsync(cancellationToken); - - TryAddToCache(_sessionCacheKey, list, item => item.SessionId, item => item.Session!); - - return list.ToLookup(t => t.SpeakerId, t => t.Session!); - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/DataLoader/SpeakerByIdDataLoader.cs b/code/complete/GraphQL/DataLoader/SpeakerByIdDataLoader.cs deleted file mode 100644 index a2e8d45..0000000 --- a/code/complete/GraphQL/DataLoader/SpeakerByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class SpeakerByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public SpeakerByIdDataLoader( - IDbContextFactory dbContextFactory, - IBatchScheduler batchScheduler, - DataLoaderOptions options) - : base(batchScheduler, options) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Speakers - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/DataLoader/SpeakerBySessionIdDataLoader.cs b/code/complete/GraphQL/DataLoader/SpeakerBySessionIdDataLoader.cs deleted file mode 100644 index 3ce320c..0000000 --- a/code/complete/GraphQL/DataLoader/SpeakerBySessionIdDataLoader.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using Microsoft.EntityFrameworkCore; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class SpeakerBySessionIdDataLoader : GroupedDataLoader - { - private static readonly string _speakerCacheKey = GetCacheKeyType(); - private readonly IDbContextFactory _dbContextFactory; - - public SpeakerBySessionIdDataLoader( - IDbContextFactory dbContextFactory, - IBatchScheduler batchScheduler, - DataLoaderOptions options) - : base(batchScheduler, options) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadGroupedBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - List list = await dbContext.Sessions - .Where(s => keys.Contains(s.Id)) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers) - .Include(s => s.Speaker) - .ToListAsync(cancellationToken); - - TryAddToCache(_speakerCacheKey, list, item => item.SpeakerId, item => item.Speaker!); - - return list.ToLookup(t => t.SessionId, t => t.Speaker!); - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/DataLoader/TrackByIdDataLoader.cs b/code/complete/GraphQL/DataLoader/TrackByIdDataLoader.cs deleted file mode 100644 index 9316fd6..0000000 --- a/code/complete/GraphQL/DataLoader/TrackByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class TrackByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public TrackByIdDataLoader( - IDbContextFactory dbContextFactory, - IBatchScheduler batchScheduler, - DataLoaderOptions options) - : base(batchScheduler, options) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Tracks - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs b/code/complete/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs deleted file mode 100644 index 06e45d4..0000000 --- a/code/complete/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL -{ - public static class ObjectFieldDescriptorExtensions - { - public static IObjectFieldDescriptor UseUpperCase( - this IObjectFieldDescriptor descriptor) - { - // TODO : we need a better API for the user. - descriptor.Extend().Definition.ResultConverters.Add( - new((_, result) => - { - if (result is string s) - { - return s.ToUpperInvariant(); - } - return result; - })); - - return descriptor; - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Extensions/UseApplicationDbContextAttribute.cs b/code/complete/GraphQL/Extensions/UseApplicationDbContextAttribute.cs deleted file mode 100644 index 19e887d..0000000 --- a/code/complete/GraphQL/Extensions/UseApplicationDbContextAttribute.cs +++ /dev/null @@ -1,12 +0,0 @@ -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Data; - -namespace ConferencePlanner.GraphQL -{ - public class UseApplicationDbContextAttribute : UseDbContextAttribute - { - public UseApplicationDbContextAttribute() : base(typeof(ApplicationDbContext)) - { - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Extensions/UseUpperCaseAttribute.cs b/code/complete/GraphQL/Extensions/UseUpperCaseAttribute.cs deleted file mode 100644 index cb0fc51..0000000 --- a/code/complete/GraphQL/Extensions/UseUpperCaseAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Reflection; -using HotChocolate.Types; -using HotChocolate.Types.Descriptors; - -namespace ConferencePlanner.GraphQL -{ - public class UseUpperCaseAttribute : ObjectFieldDescriptorAttribute - { - public override void OnConfigure( - IDescriptorContext context, - IObjectFieldDescriptor descriptor, - MemberInfo member) - => descriptor.UseUpperCase(); - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/GraphQL.csproj b/code/complete/GraphQL/GraphQL.csproj deleted file mode 100644 index cc3bc6b..0000000 --- a/code/complete/GraphQL/GraphQL.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - net6.0 - ConferencePlanner.GraphQL - enable - - - - true - $(NoWarn);1591 - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - diff --git a/code/complete/GraphQL/Imports/DataImporter.cs b/code/complete/GraphQL/Imports/DataImporter.cs deleted file mode 100644 index cc36098..0000000 --- a/code/complete/GraphQL/Imports/DataImporter.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace ConferencePlanner.GraphQL.Imports -{ - public class DataImporter - { - public async Task LoadDataAsync(ApplicationDbContext db) - { - await using var stream = File.OpenRead("Imports/NDC_London_2019.json"); - using var reader = new JsonTextReader(new StreamReader(stream)); - - JArray conference = await JArray.LoadAsync(reader); - var speakers = new Dictionary(); - - foreach (var conferenceDay in conference) - { - foreach (var roomData in conferenceDay["rooms"]!) - { - var track = new Track - { - Name = roomData["name"]!.ToString() - }; - - foreach (var sessionData in roomData["sessions"]!) - { - var session = new Session - { - Title = sessionData["title"]!.ToString(), - Abstract = sessionData["description"]!.ToString(), - StartTime = sessionData["startsAt"]!.Value(), - EndTime = sessionData["endsAt"]!.Value(), - }; - - track.Sessions.Add(session); - - foreach (var speakerData in sessionData["speakers"]!) - { - string id = speakerData["id"]!.ToString(); - - if (!speakers.TryGetValue(id, out Speaker? speaker)) - { - speaker = new Speaker - { - Name = speakerData["name"]!.ToString() - }; - - speakers.Add(id, speaker); - db.Speakers.Add(speaker); - } - - session.SessionSpeakers.Add(new SessionSpeaker - { - Speaker = speaker, - Session = session - }); - } - } - - db.Tracks.Add(track); - } - } - - await db.SaveChangesAsync(); - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Imports/DataImporterExtensions.cs b/code/complete/GraphQL/Imports/DataImporterExtensions.cs deleted file mode 100644 index 7a693f2..0000000 --- a/code/complete/GraphQL/Imports/DataImporterExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ - -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Execution.Configuration; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; - -namespace ConferencePlanner.GraphQL.Imports -{ - public static class ImportRequestExecutorBuilderExtensions - { - public static IRequestExecutorBuilder EnsureDatabaseIsCreated( - this IRequestExecutorBuilder builder) => - builder.ConfigureSchemaAsync(async (services, _, ct) => - { - IDbContextFactory factory = - services.GetRequiredService>(); - await using ApplicationDbContext dbContext = factory.CreateDbContext(); - - if (await dbContext.Database.EnsureCreatedAsync(ct)) - { - var importer = new DataImporter(); - await importer.LoadDataAsync(dbContext); - } - }); - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Imports/NDC_London_2019.json b/code/complete/GraphQL/Imports/NDC_London_2019.json deleted file mode 100644 index fea2d89..0000000 --- a/code/complete/GraphQL/Imports/NDC_London_2019.json +++ /dev/null @@ -1,8119 +0,0 @@ -[ - { - "date": "2019-01-30T00:00:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "sessions": [ - { - "id": "69229", - "title": "Keynote: Welcome to the Machine", - "description": "Information is everywhere and for many people, especially in the connected world, it is accessible freely or at a minimal cost. News outlets rely on social media to broadcast breaking news. Social media in turn relies on us to feed it with information, be it of our surroundings or our personal information. It’s become somewhat of a self-sustaining self-serving machine in which we’re all part of. It’s big data and we’re a cog in the wheel. For now of course, because with big data and cheap yet powerful hardware, AI also wants to play the game.\r\n\r\nAnd if information and knowledge is the key to success, surely this means we’re on the right path. The question is, will we notice some of the warning signs before it’s too late…", - "startsAt": "2019-01-30T09:00:00", - "endsAt": "2019-01-30T10:00:00", - "isServiceSession": false, - "isPlenumSession": true, - "speakers": [ - { - "id": "2d146e58-439f-40ec-9d5b-1dbefdeb707a", - "name": "Hadi Hariri" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - { - "id": "66591", - "title": "Much Ado about Nothing: A C# play in two acts - Act 1, starring Mads Torgersen", - "description": "Understand the history and motivation behind introducing nullable types into an existing language.\r\nThis opening act is a deep design dive where you see the twists and turns of designing such a major feature that introduces potentially breaking changes into mountains of existing code. The stage is set for a major strategic shift in how you write C# code.", - "startsAt": "2019-01-30T10:20:00", - "endsAt": "2019-01-30T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "8f64af49-a647-48e7-850b-70505308d948", - "name": "Mads Torgersen" - }, - { - "id": "be2a42b5-fc04-47c8-860b-7c503a8c6854", - "name": "Bill Wagner" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10583, - "name": "Languages" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - { - "id": "66592", - "title": "Much Ado about Nothing: A C# play in two acts. Act 2, starring Bill Wagner", - "description": "Resolve that tension by learning to love that strategic shift and put the new understanding into practice.\r\n\r\nLearn how nullable reference types affects your design decisions and how you express those decisions. Learn how to migrate an existing code base by discovering the original intent and expressing that intent in new syntax. The exciting conclusion to a world without null.", - "startsAt": "2019-01-30T11:40:00", - "endsAt": "2019-01-30T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "be2a42b5-fc04-47c8-860b-7c503a8c6854", - "name": "Bill Wagner" - }, - { - "id": "8f64af49-a647-48e7-850b-70505308d948", - "name": "Mads Torgersen" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10583, - "name": "Languages" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - { - "id": "69301", - "title": "Insecure Transit - Microservice Security", - "description": "A deep dive into some of the technical challenges and solutions to securing a microservice architecture.\r\n\r\nMicroservices are great, and they offer us lots of options for how we can build, scale and evolve our applications. On the face of it, they should also help us create much more secure applications - the ability to protect in depth is a key part of protecting systems, and microservices make this much easier. On the other hand, information that used to flow within single processes, now flows over our networks, giving us a real headache. How do we make sure our shiny new microservices architectures aren’t less secure than their monolithic predecessor.\r\n\r\nIn this talk, I outline some of the key challenges associated with microservice architectures with respect to security, and then looks at approaches to address these issues. From secret stores, time-limited credentials and better backups, to confused deputy problems, JWT tokens and service meshes, this talk looks at the state of the art for building secure microservice architectures.", - "startsAt": "2019-01-30T13:40:00", - "endsAt": "2019-01-30T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "9dac5e37-9ba2-4d4b-90a1-a004f4d51270", - "name": "Sam Newman" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - { - "id": "68804", - "title": "Life Beyond Distributed Transactions: An Apostate's Implementation", - "description": "Over a decade ago, Pat Helland authored his paper, \"Life Beyond Distributed Transactions: An Apostate's Opinion\" describing a means to coordinate activities between entities in databases when a transaction encompassing those entities wasn't feasible or possible. While the paper and subsequent talks provided great insight in the challenges of coordinating activities across entities in a single database, implementations were left as an exercise to the reader!\r\n\r\nFast forward to today, and now we have NoSQL databases, microservices, message queues and brokers, HTTP web services and more that don't (and shouldn't) support any kind of distributed transaction.\r\n\r\nIn this session, we'll look at how to implement coordination between non-transactional resources using Pat's paper as a guide, with examples in Azure Cosmos DB, Azure Service Bus, and Azure SQL Server. We'll look at a real-world example where a codebase assumed everything would be transactional and always succeed, but production proved us wrong! Finally, we'll look at advanced coordination workflows such as Sagas to achieve robust, scalable coordination across the enterprise.", - "startsAt": "2019-01-30T16:20:00", - "endsAt": "2019-01-30T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "f109dd0b-9441-4cbf-8664-19c021a6de4a", - "name": "Jimmy Bogard" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10585, - "name": "Microservices" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - { - "id": "69174", - "title": "Hack to the Future", - "description": "Infosec is a continual game of one-upmanship; we build a defence and someone breaks it so we build another one then they break that and the cycle continues. Because of this, the security controls we have at our disposal are rapidly changing and the ones we used yesterday are very often useless today.\r\n\r\nThis talk focuses on what the threats look like *today*. What are we getting wrong, how do we fix it and how do we stay on top in an environment which will be different again tomorrow to what it is today. It's a real-world look at modern defences that everyone building online applications will want to see.", - "startsAt": "2019-01-30T17:40:00", - "endsAt": "2019-01-30T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "735a4b60-42e8-4452-9480-68197372c206", - "name": "Troy Hunt" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10589, - "name": "Security" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - } - ], - "hasOnlyPlenumSessions": false - }, - { - "id": 2776, - "name": "Room 2", - "sessions": [ - { - "id": "69170", - "title": "What you need to know about ASP.NET Core 2.2", - "description": "Another new version of ASP.NET Core is here and it brings new capabilities, making it easier than ever to build and consume APIs. But there's also some hidden gems in the framework that aren't well known that you should definitely know about! \r\n\r\nDamian and David from the ASP.NET Core team are back to show you the new features plus their favourite, little-known features that don't get enough attention but will make your lives easier.", - "startsAt": "2019-01-30T10:20:00", - "endsAt": "2019-01-30T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "ad079cb0-a1a4-4051-8690-ba4975eb207a", - "name": "Damian Edwards" - }, - { - "id": "f9759467-293f-4805-a4da-c9db9d8310aa", - "name": "David Fowler" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10596, - "name": "Web" - }, - { - "id": 10586, - "name": "Microsoft" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - { - "id": "68938", - "title": "Securing Web Applications and APIs with ASP.NET Core 2.2 and 3.0", - "description": "ASP.NET Core and MVC is a mature and modern platform to build secure web applications and APIs for a while now. Starting with version 2.2, Microsoft makes big investments in the areas of standards-based authentication, single sign-on and API security by including the popular open source project IdentityServer4 in the project templates. This talk gives an overview over the various security features in ASP.NET Core but focuses in particular on the API security scenarios enabled by IdentityServer.", - "startsAt": "2019-01-30T11:40:00", - "endsAt": "2019-01-30T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "2201252d-46ba-4f0e-a519-ac8f6ea1501c", - "name": "Dominick Baier" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10589, - "name": "Security" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - { - "id": "68939", - "title": "Building Clients for OpenID Connect/OAuth 2-based Systems", - "description": "Using protocols like OpenID Connect and OAuth 2 for authentication and API access can on one hand simply your front-ends dramatically since they don’t have to deal with credentials anymore – but on the other hand introduces new challenges like choosing the right protocol flow for the given client, secure token storage as well as token lifetime management. This talk gives an overview over the best practices how to solve the above problems for both native server and client-side applications as well as browser-based applications and SPAs.", - "startsAt": "2019-01-30T13:40:00", - "endsAt": "2019-01-30T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "2201252d-46ba-4f0e-a519-ac8f6ea1501c", - "name": "Dominick Baier" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10589, - "name": "Security" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - { - "id": "66448", - "title": "Designing Nullable Reference Types in F#", - "description": "Together for C#, F# will be incorporating nullability as a concept for .NET reference types. From the F# perspective, this fits with the default non-nullness of other F# types. But compatibility with existing code makes designing this a wild ride indeed! In this talk, we'll briefly explain what nullability means for F#, some existing mitigations for null in the language, and how we must consider compatibility with everything in mind. This deep dive into language design should give you an idea about what it is like designing a nontrivial feature that improves existing code while remaining compatible with it.", - "startsAt": "2019-01-30T15:00:00", - "endsAt": "2019-01-30T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "cdeb4f0a-4f3e-4bd3-be85-b8528a5fdfcb", - "name": "Phillip Carter" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10583, - "name": "Languages" - }, - { - "id": 10580, - "name": "Functional Programming" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - { - "id": "68940", - "title": "Domain-Driven Design: Hidden Lessons from the Big Blue Book", - "description": "We are entering an incredible new era of digital product development where users expect a seamless experience across all of their touchable, wearable, and voice-activated devices. How can we learn to develop software effectively in this new digital-by-default world? \r\n\r\nWhat if the answers are hidden away as secret messages in a 15 year old book? \r\n\r\nAre bounded contexts really used to design loosely coupled code, or are they one of the most powerful organisation design tools used to enable autonomous, self-organising teams? Are core domains just academic jargon that get in the way of shiny technical practices like event sourcing, or is understanding business core domains one of the key differentiators between high-performing delivery teams and the rest of us?\r\n\r\nLet’s go on an adventure and see if the big blue big and can help us in this brave new world.", - "startsAt": "2019-01-30T16:20:00", - "endsAt": "2019-01-30T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "b9d7090c-afe3-4278-ae79-d85987b4aae5", - "name": "Nick Tune" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10566, - "name": "Architecture" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - { - "id": "61516", - "title": "Panel discussion on the future of .NET", - "description": "Panel discussion with four experts in the field on the current state of the art and the where .NET and related technologies are heading.\r\n\r\nWe will discuss cross platform development, new features, performance, versioning issues of .NET Core, what’s going to happen with full framework, Blazor, how .NET stands up against competing technologies and where it is all going. \r\n\r\nYou won't cram more info into a session than this, come spend a great hour with us.", - "startsAt": "2019-01-30T17:40:00", - "endsAt": "2019-01-30T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "de972e57-7765-4c38-9dcd-5981587c1433", - "name": "Bryan Hogan" - }, - { - "id": "70590ad0-b1b3-40b8-b05d-b58722ef9d9d", - "name": "Mark Rendle" - }, - { - "id": "cb70e0f2-0f0b-4b8f-b3f4-a9326309a1b2", - "name": "Tess Ferrandez-Norlander" - }, - { - "id": "8b1783d3-15ce-41dc-b989-386759803d97", - "name": "Oren Novotny" - }, - { - "id": "8f64af49-a647-48e7-850b-70505308d948", - "name": "Mads Torgersen" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10596, - "name": "Web" - }, - { - "id": 10586, - "name": "Microsoft" - }, - { - "id": 10572, - "name": "Cross-Platform" - }, - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - } - ], - "hasOnlyPlenumSessions": false - }, - { - "id": 2777, - "name": "Room 3", - "sessions": [ - { - "id": "68546", - "title": "Observability and the Development Process", - "description": "Historically, monitoring has been thought of as an afterthought of the software development cycle: something owned by the ops side of the room. But instead of trying to predict the various ways something might go sideways right before release and crafting dashboards to prepare, what might it look like to use answers about our system to figure out what to build, and how to build it, and whom for?\r\n\r\nObservability is the practice of understanding the internal state of a system via knowledge of its external outputs -- and is something that should be built into the process of crafting software from the very beginning.\r\n\r\nIn this talk, we'll discuss what this might look like in practice by using Honeycomb as a case study: how we rely on visibility into our system to inform planning during the development process, to observe the impact of new changes during and after release, and, of course, debug. We'll start by describing the problems faced by a SaaS platform like ours, then run through some specific instrumentation practices that we love and have used successfully to gain the visibility we need into our system’s day-to-day operations.", - "startsAt": "2019-01-30T10:20:00", - "endsAt": "2019-01-30T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "e5d55e8e-93d1-4a53-bbec-10e4beaf5fd8", - "name": "Christine Yen" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10592, - "name": "Testing" - }, - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10575, - "name": "DevOps" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - { - "id": "68745", - "title": "Distributed Tracing: How the Pros Debug Concurrent and Distributed Systems", - "description": "As more and more developers move to distributed architectures such as micro services, distributed actor systems, and so forth it becomes increasingly complex to understand, debug, and diagnose.\r\n\r\nIn this talk we're going to introduce the emerging OpenTracing standard and talk about how you can instrument your applications to help visualize every operation, even across process and service boundaries. We'll also introduce Zipkin, one of the most popular implementations of the OpenTracing standard. ", - "startsAt": "2019-01-30T11:40:00", - "endsAt": "2019-01-30T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "5a279f06-e4d2-449c-879d-eedec29cd401", - "name": "Aaron Stannard" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10585, - "name": "Microservices" - }, - { - "id": 10572, - "name": "Cross-Platform" - }, - { - "id": 10575, - "name": "DevOps" - }, - { - "id": 10566, - "name": "Architecture" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - { - "id": "58654", - "title": "Icons and the Web: Symbols of the Modern Age", - "description": "Icons have been a staple of software for decades, and come in as many varieties as the tools used to make them. From humble beginnings as precisely-pixelated pictograms, icons are now entering a renaissance of high-density displays, vector formats, and an almost cult-like following. In this session, you'll learn the inner workings of modern icon design, explore various techniques for adding symbology to your web apps, and discover how to bring your interfaces into the modern age!", - "startsAt": "2019-01-30T13:40:00", - "endsAt": "2019-01-30T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "17a4cdca-0332-4607-9de3-993d25ccc459", - "name": "Tim G. Thomas" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10594, - "name": "UI" - }, - { - "id": 10595, - "name": "UX" - }, - { - "id": 10596, - "name": "Web" - }, - { - "id": 10574, - "name": "Design" - }, - { - "id": 10593, - "name": "Tools" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - { - "id": "68734", - "title": "Infrastructure as TypeScript", - "description": "For almost a decade \"Infrastructure as Code\" has been a DevOps buzzword - but the myriad tools in share a dirty little secret... there's no actual code! Few people like \"programming\" YAML or JSON (even the human-friendly variants!), and even fewer like having to reverse-engineer ways to apply known good development practices to tools which resist it at all cost.\r\n\r\nSo, what if things were different,and programming infrastructure was more like real programming, with real programming languages like TypeScript? What if you defined Lambda functions by actually writing lambdas, created abstractions using complex types, and could take advantage of existing tools for modularity, linting, refactoring and testing?\r\n\r\nEnter Pulumi, an open-source deployment engine which enables all these things using TypeScript, Python or Go!\r\n\r\nIn this talk, we'll look at how you can write TypeScript code using Pulumi to provision traditional cloud infrastructure, manage Kubernetes and build portable \"serverless\" applications - all with minimal YAML in sight! We'll look at deploying to multiple regions of the same cloud, and how to build abstractions allowing multi-cloud to be a reality.", - "startsAt": "2019-01-30T15:00:00", - "endsAt": "2019-01-30T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "82242efa-dc48-4c4b-8e26-6b93bbe5623a", - "name": "James Nugent" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10582, - "name": "JavaScript" - }, - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10590, - "name": "Serverless" - }, - { - "id": 10575, - "name": "DevOps" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - { - "id": "68493", - "title": "The tech future is diverse", - "description": "By 2020, there will be 4 times more devices connected to the Internet around the world. While technology impacts our everyday life in almost every way, the solutions we create fails to reflect our society or the world we live in. Instead, they often reinforce stereotypes, prejudice, and differences. In this talk, we will look into the lack of diversity and how diversity will make us more suited to solve problems and meet the needs of our society. We will address the culture in our communities, the reasons why minorities quit, and the importance of diversity in tech.", - "startsAt": "2019-01-30T16:20:00", - "endsAt": "2019-01-30T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "50fd0b93-cece-43b5-a7f6-f9ccca500f91", - "name": "Tannaz N. Roshandel" - }, - { - "id": "daafd2d6-0eb2-4eb4-834c-76d51734f6f7", - "name": "Line Moseng" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10578, - "name": "Ethics" - }, - { - "id": 10588, - "name": "People" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - { - "id": "62178", - "title": "Patterns for Resilient Architecture", - "description": "We have traditionally built robust software systems by trying to avoid mistakes and by dodging failures when they occur in production or by testing parts of the system in isolation from one another. Modern methods and techniques take a very different approach based on resiliency, which promotes embracing failure instead of trying to avoid it. Resilient architectures enhance observability, leverage well-known patterns such as graceful degradation, timeouts and circuit breakers and embrace chaos engineering, a discipline that promotes breaking things on purpose in order to learn how to build more resilient systems. In this session, will review the most useful patterns for building resilient software systems and I will introduce chaos engineering methodology and especially show the audience how they can benefit from breaking things on purpose.", - "startsAt": "2019-01-30T17:40:00", - "endsAt": "2019-01-30T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "e4fd745a-8a49-43d3-8763-020a4f3512ab", - "name": "Adrian Hornsby" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10585, - "name": "Microservices" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10575, - "name": "DevOps" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - } - ], - "hasOnlyPlenumSessions": false - }, - { - "id": 2778, - "name": "Room 4", - "sessions": [ - { - "id": "66715", - "title": "GraphQL Will Do To REST What JSON Did To XML", - "description": "Why GraphQL will become the new standard for accessing external data in your application. I will show how using GraphQL instead of REST services the development process becomes even more declarative as GraphQL will take away the (imperative) hassle of tying data from multiple endpoints together. This will increase the level of complexity in frontend development, while also increasing the performance of the application.\r\n", - "startsAt": "2019-01-30T11:40:00", - "endsAt": "2019-01-30T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "4bb972aa-3d92-45ff-95b4-68ed3ca86e9e", - "name": "Roy Derks" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10582, - "name": "JavaScript" - }, - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10587, - "name": "Mobile" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - { - "id": "63252", - "title": "Dynamic Runtime Code with Roslyn", - "description": "A possibly overlooked feature of the Roslyn compiler is the ability to generate, compile, and load new types at runtime. Sure, we've always had *some* ability to use dynamic code in .Net, but the existing techniques were either slow (Reflection) or daunting to use (IL generation or Expressions). Now though, we can just use C# in a way that's both more approachable for more developers and lends itself to more ambitious levels of dynamic behavior. In this talk I'll show some of the ways I've been using this technique to create more efficient, low allocation application frameworks. We'll also dive into the Utf8Json library already uses this approach today in its support for very highly efficient Json parsing.", - "startsAt": "2019-01-30T13:40:00", - "endsAt": "2019-01-30T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "cf80d55a-62bf-4be2-a986-27d217129faf", - "name": "Jeremy Miller" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - { - "id": "66433", - "title": "Pragmatic Performance: When to care about perf, and what to do about it.", - "description": "As a developer you often here both that performance is important, but also that you shouldn't worry about performance up front, so when is the right time to think about it? And if the time is right, what are you actually supposed to do?\r\n\r\nIf you're interested to hear about a pragmatic approach to performance, this talk will explain when is the right time to think about benchmarking, but more importantly will run through how to correctly benchmark .NET code so any decisions made will be based on information about your code that is trustworthy.\r\n\r\nAdditionally you'll also find out about some of the common, and some of the unknown, performance pitfalls of the .NET Framework and we'll discuss the true meaning behind the phrase \"premature optimization is the root of all evil\".", - "startsAt": "2019-01-30T15:00:00", - "endsAt": "2019-01-30T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "604507ba-fd96-4c48-a29a-67a95760d888", - "name": "David Wengier" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - { - "id": "59857", - "title": "CSS Grid - What is this Magic?!", - "description": "We’ve all heard a lot in the last year about a new advancement in the world of CSS, called CSS Grid. Starting off at whispers, we’re now starting to hear it as a deafening roar as more and more developers write about it, talk about it, share it and start using it. In the world of front end, I see it everywhere I turn and am excited as I start to use it in my own projects.\r\n\r\nBut what does this new CSS specification mean for software developers, and why should you care about it? In the world of tech today, we can do so many amazing things and use whatever language we choose across a wide range of devices and platforms. Whether it’s the advent of React and React Native, or frameworks like Electron, it’s easier than ever to build one app that works on multiple platforms with the language we know and work with best. The ability to do this also expands to styling apps on any platform using CSS, and therefore being able to utilise the magical thing that is\r\nCSS Grid.\r\n\r\nThe reason CSS Grid is gaining so much attention, is because it’s a game changer for front end and layouts. With a few simple lines of code, we can now create imaginative, dynamic, responsive layouts (yep, I know that’s a lot of buzz words). While a lot of people are calling this the new ‘table layout’, grid gives us so much more, with the ability to spread cells across columns and rows to whatever size you choose, dictate which direction new items flow, allow cells to move around to fit in place and even tell certain cells exactly where they need to sit.\r\n\r\nWhile there is so much to worry about when developing an app, CSS Grid means that you can worry less about building the layout on the front end, and more about making sure the back end works well. Let me show you how the magic works.", - "startsAt": "2019-01-30T16:20:00", - "endsAt": "2019-01-30T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "d387e75c-ed26-4dc6-8612-0f18abdfd9f5", - "name": "Amy Kapernick" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10596, - "name": "Web" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - { - "id": "68826", - "title": "A lap around Azure Devops", - "description": "Azure DevOps (previously known as Visual Studio Team Services) is a broad product suite with tools that assists small and large software development teams that want to deliver high quality software at a rapid speed. \r\n\r\nIn session we will walk through all major features in Azure DevOps, such as Azure Boards, Azure Pipelines and Azure Repos, and look at how we can continuously deliver value to or end users and implement DevOps practices such as Infrastructure as Code and testing in production using Azure.", - "startsAt": "2019-01-30T17:40:00", - "endsAt": "2019-01-30T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "54973c7d-2fe5-4a49-9a1c-a52afed4d6a4", - "name": "Jakob Ehn" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10571, - "name": "Continuous Delivery" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10564, - "name": "Agile" - }, - { - "id": 10575, - "name": "DevOps" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - } - ], - "hasOnlyPlenumSessions": false - }, - { - "id": 2779, - "name": "Room 5", - "sessions": [ - { - "id": "67422", - "title": "What You Need to Know About Open Source—Trust Me, I'm a Lawyer", - "description": "Open source tools. We all use them. Whether an entire framework, a focused toolkit, or a simple custom component from GitHub, npm, or NuGet, the opportunity to improve our development speed while learning new things from open source projects is enticing.\r\n\r\nBut what does “open source” truly mean? What are our rights and limitations as open source consumers to use, modify, and redistribute these tools in a professional environment? The answer depends upon the OSS author's own decisions regarding project licensing. Come investigate the core principles of open source development and consumption while comparing and contrasting some of the more popular licenses in use today. Learn to make better decisions for your organization by becoming informed of how best to leverage the open source works of others and also how to properly license your own.", - "startsAt": "2019-01-30T10:20:00", - "endsAt": "2019-01-30T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "b3761cce-aaeb-4ca4-a5b3-40f162f6bcf8", - "name": "Jeff Strauss" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10591, - "name": "Soft Skills" - }, - { - "id": 10588, - "name": "People" - }, - { - "id": 10578, - "name": "Ethics" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - { - "id": "66768", - "title": "Teaching New Tricks – How to enhance the skills of experienced developers", - "description": "It’s easy to forget what it felt like when you were a beginner. This lively dog-based* talk is about the rewards and pitfalls involved in introducing pair programming, TDD and an agile development approach to experienced developers who are used to working in a different way. It includes several practical suggestions of how to help and convince less agile-experienced colleagues.\r\nBased on my experience as a consultant technical lead, the aim is to help you to move your team members to a state of childlike fearlessness where learning is fun; is embedded in everything you do; and applies to all team members regardless of experience. \r\n*It turns out that images of dogs can be used to illustrate an astonishing variety of concepts!", - "startsAt": "2019-01-30T11:40:00", - "endsAt": "2019-01-30T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "403fae21-0856-432a-b35c-0949ebaac53c", - "name": "Clare Sudbery" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10564, - "name": "Agile" - }, - { - "id": 10591, - "name": "Soft Skills" - }, - { - "id": 10588, - "name": "People" - }, - { - "id": 10578, - "name": "Ethics" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - { - "id": "58982", - "title": "Reading other peoples code", - "description": "Someone else's code. Even worse, thousands of lines, maybe hundreds of files of other peoples code. Is there a way to methodically read and understand other peoples work, build their mental models? In this talk I will go through techniques I have developed throughout 18 years of programming. Hopefully you will walk away with a plan on how to approach a new code base. But even more I hope you walk away with a feeling of curiosity, wanting to get to know your fellow programmers through their code.", - "startsAt": "2019-01-30T13:40:00", - "endsAt": "2019-01-30T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "0eaa4bb2-cb2a-4b76-800d-de8b1dfdb50c", - "name": "Patricia Aas" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10572, - "name": "Cross-Platform" - }, - { - "id": 10583, - "name": "Languages" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - { - "id": "69253", - "title": "Power BI for Developers", - "description": "Integrate, Extend, Embed!\r\n\r\nIn this session, you will learn how developers can deliver real-time dashboards, create custom visuals and embed rich interactive analytics in their apps with Power BI. This presentation specifically targets experienced app developers, and also those curious to understand what developers can achieve with Power BI. Numerous demonstrations will put the theory into action.", - "startsAt": "2019-01-30T15:00:00", - "endsAt": "2019-01-30T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "cc7c7f29-87fc-44b2-a62b-a6ea4a332ea9", - "name": "Peter Myers" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10565, - "name": "AI" - }, - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10573, - "name": "Database" - }, - { - "id": 10586, - "name": "Microsoft" - }, - { - "id": 10596, - "name": "Web" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - { - "id": "67909", - "title": "Build vs Buy: Software Systems at Jurassic Park", - "description": "We were so preoccupied with whether we could, we didn’t stop to think if we should. Nowhere at Jurassic Park was this more true than how we developed software. Having the wrong software and support structures was a key factor in the failures of our first park. We were entrepreneurs launching something new and architects integrating an enterprise. And our decisions had lasting consequences. Deciding which problems were worth our time was foundational to our failure.\r\n\r\nJoin us for a retrospective of software systems at Jurassic Park. We’ll dig into case studies and explore our successes and failures. We’ll uncover the options, costs, and risks inherent in deciding what software to build, what to buy, and alternatives in between. We’ll explore the opportunity cost of building systems, the sustainability of open-source, and the risks of vendor lock-in. You’ll leave equipped to make better decisions and avoid the pitfalls we made at Jurassic Park.", - "startsAt": "2019-01-30T17:40:00", - "endsAt": "2019-01-30T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "5f017dbb-6821-480b-a750-82a0f15fa1b2", - "name": "Todd Gardner" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10588, - "name": "People" - }, - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10579, - "name": "Fun" - }, - { - "id": 10591, - "name": "Soft Skills" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - } - ], - "hasOnlyPlenumSessions": false - }, - { - "id": 2780, - "name": "Room 6", - "sessions": [ - { - "id": "67163", - "title": "Workshop: An introduction to Kubernetes on Google Cloud with Docker - Part 1/2", - "description": "Do you want to deploy your own application in the cloud, but don't know where you should start? This workshop is for you!\r\n\r\nIn this workshop you will create your first Kubernetes cluster with Docker images in Google Cloud. By using Docker images, you can build and deploy your application without worrying about the environment on the server. We will create a cluster containing a frontend web application and a backend.\r\n\r\n This workshop does not require knowledge about Docker, Kubernetes or Google Cloud.\r\n\r\nYou will need to bring your own computer to attend the workshop.", - "startsAt": "2019-01-30T10:20:00", - "endsAt": "2019-01-30T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "8c7086a0-fd3b-4c11-b085-1182faa39e7b", - "name": "Ingrid Guren" - }, - { - "id": "daafd2d6-0eb2-4eb4-834c-76d51734f6f7", - "name": "Line Moseng" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10569, - "name": "Cloud" - } - ], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - { - "id": "66459", - "title": "Getting Started with Cosmos DB + EF Core", - "description": "Cosmos DB is great and awesomely fast. Wouldn't be even more amazing if we could use our beloved entity framework to manage it? Let see how we can wire it up and get started", - "startsAt": "2019-01-30T15:00:00", - "endsAt": "2019-01-30T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "c739e2f1-ecf5-43e1-abcf-90cf13dd7b8f", - "name": "Thiago Passos" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10590, - "name": "Serverless" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10573, - "name": "Database" - } - ], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - { - "id": "69255", - "title": "Workshop: Embedding Power BI Analytics - Part 1/2", - "description": "This instructor-led workshop focuses on development practices for embedding Power BI reports, dashboards and the Q&A experience, and working with the Power BI JavaScript API.\r\n\r\nThis workshop is designed for web developers experienced with ASP.NET, Visual C#, HTML and JavaScript. You are required to bring your own PC, with Visual Studio 2015 (or later) with web tools installed.", - "startsAt": "2019-01-30T16:20:00", - "endsAt": "2019-01-30T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "cc7c7f29-87fc-44b2-a62b-a6ea4a332ea9", - "name": "Peter Myers" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10596, - "name": "Web" - }, - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10573, - "name": "Database" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10582, - "name": "JavaScript" - }, - { - "id": 10586, - "name": "Microsoft" - } - ], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - } - ], - "hasOnlyPlenumSessions": false - } - ], - "timeSlots": [ - { - "slotStart": "09:00:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "69229", - "title": "Keynote: Welcome to the Machine", - "description": "Information is everywhere and for many people, especially in the connected world, it is accessible freely or at a minimal cost. News outlets rely on social media to broadcast breaking news. Social media in turn relies on us to feed it with information, be it of our surroundings or our personal information. It’s become somewhat of a self-sustaining self-serving machine in which we’re all part of. It’s big data and we’re a cog in the wheel. For now of course, because with big data and cheap yet powerful hardware, AI also wants to play the game.\r\n\r\nAnd if information and knowledge is the key to success, surely this means we’re on the right path. The question is, will we notice some of the warning signs before it’s too late…", - "startsAt": "2019-01-30T09:00:00", - "endsAt": "2019-01-30T10:00:00", - "isServiceSession": false, - "isPlenumSession": true, - "speakers": [ - { - "id": "2d146e58-439f-40ec-9d5b-1dbefdeb707a", - "name": "Hadi Hariri" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - } - ] - }, - { - "slotStart": "10:20:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "66591", - "title": "Much Ado about Nothing: A C# play in two acts - Act 1, starring Mads Torgersen", - "description": "Understand the history and motivation behind introducing nullable types into an existing language.\r\nThis opening act is a deep design dive where you see the twists and turns of designing such a major feature that introduces potentially breaking changes into mountains of existing code. The stage is set for a major strategic shift in how you write C# code.", - "startsAt": "2019-01-30T10:20:00", - "endsAt": "2019-01-30T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "8f64af49-a647-48e7-850b-70505308d948", - "name": "Mads Torgersen" - }, - { - "id": "be2a42b5-fc04-47c8-860b-7c503a8c6854", - "name": "Bill Wagner" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10583, - "name": "Languages" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "69170", - "title": "What you need to know about ASP.NET Core 2.2", - "description": "Another new version of ASP.NET Core is here and it brings new capabilities, making it easier than ever to build and consume APIs. But there's also some hidden gems in the framework that aren't well known that you should definitely know about! \r\n\r\nDamian and David from the ASP.NET Core team are back to show you the new features plus their favourite, little-known features that don't get enough attention but will make your lives easier.", - "startsAt": "2019-01-30T10:20:00", - "endsAt": "2019-01-30T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "ad079cb0-a1a4-4051-8690-ba4975eb207a", - "name": "Damian Edwards" - }, - { - "id": "f9759467-293f-4805-a4da-c9db9d8310aa", - "name": "David Fowler" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10596, - "name": "Web" - }, - { - "id": 10586, - "name": "Microsoft" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "68546", - "title": "Observability and the Development Process", - "description": "Historically, monitoring has been thought of as an afterthought of the software development cycle: something owned by the ops side of the room. But instead of trying to predict the various ways something might go sideways right before release and crafting dashboards to prepare, what might it look like to use answers about our system to figure out what to build, and how to build it, and whom for?\r\n\r\nObservability is the practice of understanding the internal state of a system via knowledge of its external outputs -- and is something that should be built into the process of crafting software from the very beginning.\r\n\r\nIn this talk, we'll discuss what this might look like in practice by using Honeycomb as a case study: how we rely on visibility into our system to inform planning during the development process, to observe the impact of new changes during and after release, and, of course, debug. We'll start by describing the problems faced by a SaaS platform like ours, then run through some specific instrumentation practices that we love and have used successfully to gain the visibility we need into our system’s day-to-day operations.", - "startsAt": "2019-01-30T10:20:00", - "endsAt": "2019-01-30T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "e5d55e8e-93d1-4a53-bbec-10e4beaf5fd8", - "name": "Christine Yen" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10592, - "name": "Testing" - }, - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10575, - "name": "DevOps" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "67422", - "title": "What You Need to Know About Open Source—Trust Me, I'm a Lawyer", - "description": "Open source tools. We all use them. Whether an entire framework, a focused toolkit, or a simple custom component from GitHub, npm, or NuGet, the opportunity to improve our development speed while learning new things from open source projects is enticing.\r\n\r\nBut what does “open source” truly mean? What are our rights and limitations as open source consumers to use, modify, and redistribute these tools in a professional environment? The answer depends upon the OSS author's own decisions regarding project licensing. Come investigate the core principles of open source development and consumption while comparing and contrasting some of the more popular licenses in use today. Learn to make better decisions for your organization by becoming informed of how best to leverage the open source works of others and also how to properly license your own.", - "startsAt": "2019-01-30T10:20:00", - "endsAt": "2019-01-30T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "b3761cce-aaeb-4ca4-a5b3-40f162f6bcf8", - "name": "Jeff Strauss" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10591, - "name": "Soft Skills" - }, - { - "id": 10588, - "name": "People" - }, - { - "id": 10578, - "name": "Ethics" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - }, - { - "id": 2780, - "name": "Room 6", - "session": { - "id": "67163", - "title": "Workshop: An introduction to Kubernetes on Google Cloud with Docker - Part 1/2", - "description": "Do you want to deploy your own application in the cloud, but don't know where you should start? This workshop is for you!\r\n\r\nIn this workshop you will create your first Kubernetes cluster with Docker images in Google Cloud. By using Docker images, you can build and deploy your application without worrying about the environment on the server. We will create a cluster containing a frontend web application and a backend.\r\n\r\n This workshop does not require knowledge about Docker, Kubernetes or Google Cloud.\r\n\r\nYou will need to bring your own computer to attend the workshop.", - "startsAt": "2019-01-30T10:20:00", - "endsAt": "2019-01-30T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "8c7086a0-fd3b-4c11-b085-1182faa39e7b", - "name": "Ingrid Guren" - }, - { - "id": "daafd2d6-0eb2-4eb4-834c-76d51734f6f7", - "name": "Line Moseng" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10569, - "name": "Cloud" - } - ], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - "index": 5 - } - ] - }, - { - "slotStart": "11:40:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "66592", - "title": "Much Ado about Nothing: A C# play in two acts. Act 2, starring Bill Wagner", - "description": "Resolve that tension by learning to love that strategic shift and put the new understanding into practice.\r\n\r\nLearn how nullable reference types affects your design decisions and how you express those decisions. Learn how to migrate an existing code base by discovering the original intent and expressing that intent in new syntax. The exciting conclusion to a world without null.", - "startsAt": "2019-01-30T11:40:00", - "endsAt": "2019-01-30T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "be2a42b5-fc04-47c8-860b-7c503a8c6854", - "name": "Bill Wagner" - }, - { - "id": "8f64af49-a647-48e7-850b-70505308d948", - "name": "Mads Torgersen" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10583, - "name": "Languages" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "68938", - "title": "Securing Web Applications and APIs with ASP.NET Core 2.2 and 3.0", - "description": "ASP.NET Core and MVC is a mature and modern platform to build secure web applications and APIs for a while now. Starting with version 2.2, Microsoft makes big investments in the areas of standards-based authentication, single sign-on and API security by including the popular open source project IdentityServer4 in the project templates. This talk gives an overview over the various security features in ASP.NET Core but focuses in particular on the API security scenarios enabled by IdentityServer.", - "startsAt": "2019-01-30T11:40:00", - "endsAt": "2019-01-30T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "2201252d-46ba-4f0e-a519-ac8f6ea1501c", - "name": "Dominick Baier" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10589, - "name": "Security" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "68745", - "title": "Distributed Tracing: How the Pros Debug Concurrent and Distributed Systems", - "description": "As more and more developers move to distributed architectures such as micro services, distributed actor systems, and so forth it becomes increasingly complex to understand, debug, and diagnose.\r\n\r\nIn this talk we're going to introduce the emerging OpenTracing standard and talk about how you can instrument your applications to help visualize every operation, even across process and service boundaries. We'll also introduce Zipkin, one of the most popular implementations of the OpenTracing standard. ", - "startsAt": "2019-01-30T11:40:00", - "endsAt": "2019-01-30T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "5a279f06-e4d2-449c-879d-eedec29cd401", - "name": "Aaron Stannard" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10585, - "name": "Microservices" - }, - { - "id": 10572, - "name": "Cross-Platform" - }, - { - "id": 10575, - "name": "DevOps" - }, - { - "id": 10566, - "name": "Architecture" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "66715", - "title": "GraphQL Will Do To REST What JSON Did To XML", - "description": "Why GraphQL will become the new standard for accessing external data in your application. I will show how using GraphQL instead of REST services the development process becomes even more declarative as GraphQL will take away the (imperative) hassle of tying data from multiple endpoints together. This will increase the level of complexity in frontend development, while also increasing the performance of the application.\r\n", - "startsAt": "2019-01-30T11:40:00", - "endsAt": "2019-01-30T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "4bb972aa-3d92-45ff-95b4-68ed3ca86e9e", - "name": "Roy Derks" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10582, - "name": "JavaScript" - }, - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10587, - "name": "Mobile" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "66768", - "title": "Teaching New Tricks – How to enhance the skills of experienced developers", - "description": "It’s easy to forget what it felt like when you were a beginner. This lively dog-based* talk is about the rewards and pitfalls involved in introducing pair programming, TDD and an agile development approach to experienced developers who are used to working in a different way. It includes several practical suggestions of how to help and convince less agile-experienced colleagues.\r\nBased on my experience as a consultant technical lead, the aim is to help you to move your team members to a state of childlike fearlessness where learning is fun; is embedded in everything you do; and applies to all team members regardless of experience. \r\n*It turns out that images of dogs can be used to illustrate an astonishing variety of concepts!", - "startsAt": "2019-01-30T11:40:00", - "endsAt": "2019-01-30T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "403fae21-0856-432a-b35c-0949ebaac53c", - "name": "Clare Sudbery" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10564, - "name": "Agile" - }, - { - "id": 10591, - "name": "Soft Skills" - }, - { - "id": 10588, - "name": "People" - }, - { - "id": 10578, - "name": "Ethics" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - } - ] - }, - { - "slotStart": "13:40:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "69301", - "title": "Insecure Transit - Microservice Security", - "description": "A deep dive into some of the technical challenges and solutions to securing a microservice architecture.\r\n\r\nMicroservices are great, and they offer us lots of options for how we can build, scale and evolve our applications. On the face of it, they should also help us create much more secure applications - the ability to protect in depth is a key part of protecting systems, and microservices make this much easier. On the other hand, information that used to flow within single processes, now flows over our networks, giving us a real headache. How do we make sure our shiny new microservices architectures aren’t less secure than their monolithic predecessor.\r\n\r\nIn this talk, I outline some of the key challenges associated with microservice architectures with respect to security, and then looks at approaches to address these issues. From secret stores, time-limited credentials and better backups, to confused deputy problems, JWT tokens and service meshes, this talk looks at the state of the art for building secure microservice architectures.", - "startsAt": "2019-01-30T13:40:00", - "endsAt": "2019-01-30T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "9dac5e37-9ba2-4d4b-90a1-a004f4d51270", - "name": "Sam Newman" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "68939", - "title": "Building Clients for OpenID Connect/OAuth 2-based Systems", - "description": "Using protocols like OpenID Connect and OAuth 2 for authentication and API access can on one hand simply your front-ends dramatically since they don’t have to deal with credentials anymore – but on the other hand introduces new challenges like choosing the right protocol flow for the given client, secure token storage as well as token lifetime management. This talk gives an overview over the best practices how to solve the above problems for both native server and client-side applications as well as browser-based applications and SPAs.", - "startsAt": "2019-01-30T13:40:00", - "endsAt": "2019-01-30T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "2201252d-46ba-4f0e-a519-ac8f6ea1501c", - "name": "Dominick Baier" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10589, - "name": "Security" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "58654", - "title": "Icons and the Web: Symbols of the Modern Age", - "description": "Icons have been a staple of software for decades, and come in as many varieties as the tools used to make them. From humble beginnings as precisely-pixelated pictograms, icons are now entering a renaissance of high-density displays, vector formats, and an almost cult-like following. In this session, you'll learn the inner workings of modern icon design, explore various techniques for adding symbology to your web apps, and discover how to bring your interfaces into the modern age!", - "startsAt": "2019-01-30T13:40:00", - "endsAt": "2019-01-30T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "17a4cdca-0332-4607-9de3-993d25ccc459", - "name": "Tim G. Thomas" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10594, - "name": "UI" - }, - { - "id": 10595, - "name": "UX" - }, - { - "id": 10596, - "name": "Web" - }, - { - "id": 10574, - "name": "Design" - }, - { - "id": 10593, - "name": "Tools" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "63252", - "title": "Dynamic Runtime Code with Roslyn", - "description": "A possibly overlooked feature of the Roslyn compiler is the ability to generate, compile, and load new types at runtime. Sure, we've always had *some* ability to use dynamic code in .Net, but the existing techniques were either slow (Reflection) or daunting to use (IL generation or Expressions). Now though, we can just use C# in a way that's both more approachable for more developers and lends itself to more ambitious levels of dynamic behavior. In this talk I'll show some of the ways I've been using this technique to create more efficient, low allocation application frameworks. We'll also dive into the Utf8Json library already uses this approach today in its support for very highly efficient Json parsing.", - "startsAt": "2019-01-30T13:40:00", - "endsAt": "2019-01-30T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "cf80d55a-62bf-4be2-a986-27d217129faf", - "name": "Jeremy Miller" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "58982", - "title": "Reading other peoples code", - "description": "Someone else's code. Even worse, thousands of lines, maybe hundreds of files of other peoples code. Is there a way to methodically read and understand other peoples work, build their mental models? In this talk I will go through techniques I have developed throughout 18 years of programming. Hopefully you will walk away with a plan on how to approach a new code base. But even more I hope you walk away with a feeling of curiosity, wanting to get to know your fellow programmers through their code.", - "startsAt": "2019-01-30T13:40:00", - "endsAt": "2019-01-30T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "0eaa4bb2-cb2a-4b76-800d-de8b1dfdb50c", - "name": "Patricia Aas" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10572, - "name": "Cross-Platform" - }, - { - "id": 10583, - "name": "Languages" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - } - ] - }, - { - "slotStart": "15:00:00", - "rooms": [ - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "66448", - "title": "Designing Nullable Reference Types in F#", - "description": "Together for C#, F# will be incorporating nullability as a concept for .NET reference types. From the F# perspective, this fits with the default non-nullness of other F# types. But compatibility with existing code makes designing this a wild ride indeed! In this talk, we'll briefly explain what nullability means for F#, some existing mitigations for null in the language, and how we must consider compatibility with everything in mind. This deep dive into language design should give you an idea about what it is like designing a nontrivial feature that improves existing code while remaining compatible with it.", - "startsAt": "2019-01-30T15:00:00", - "endsAt": "2019-01-30T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "cdeb4f0a-4f3e-4bd3-be85-b8528a5fdfcb", - "name": "Phillip Carter" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10583, - "name": "Languages" - }, - { - "id": 10580, - "name": "Functional Programming" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "68734", - "title": "Infrastructure as TypeScript", - "description": "For almost a decade \"Infrastructure as Code\" has been a DevOps buzzword - but the myriad tools in share a dirty little secret... there's no actual code! Few people like \"programming\" YAML or JSON (even the human-friendly variants!), and even fewer like having to reverse-engineer ways to apply known good development practices to tools which resist it at all cost.\r\n\r\nSo, what if things were different,and programming infrastructure was more like real programming, with real programming languages like TypeScript? What if you defined Lambda functions by actually writing lambdas, created abstractions using complex types, and could take advantage of existing tools for modularity, linting, refactoring and testing?\r\n\r\nEnter Pulumi, an open-source deployment engine which enables all these things using TypeScript, Python or Go!\r\n\r\nIn this talk, we'll look at how you can write TypeScript code using Pulumi to provision traditional cloud infrastructure, manage Kubernetes and build portable \"serverless\" applications - all with minimal YAML in sight! We'll look at deploying to multiple regions of the same cloud, and how to build abstractions allowing multi-cloud to be a reality.", - "startsAt": "2019-01-30T15:00:00", - "endsAt": "2019-01-30T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "82242efa-dc48-4c4b-8e26-6b93bbe5623a", - "name": "James Nugent" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10582, - "name": "JavaScript" - }, - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10590, - "name": "Serverless" - }, - { - "id": 10575, - "name": "DevOps" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "66433", - "title": "Pragmatic Performance: When to care about perf, and what to do about it.", - "description": "As a developer you often here both that performance is important, but also that you shouldn't worry about performance up front, so when is the right time to think about it? And if the time is right, what are you actually supposed to do?\r\n\r\nIf you're interested to hear about a pragmatic approach to performance, this talk will explain when is the right time to think about benchmarking, but more importantly will run through how to correctly benchmark .NET code so any decisions made will be based on information about your code that is trustworthy.\r\n\r\nAdditionally you'll also find out about some of the common, and some of the unknown, performance pitfalls of the .NET Framework and we'll discuss the true meaning behind the phrase \"premature optimization is the root of all evil\".", - "startsAt": "2019-01-30T15:00:00", - "endsAt": "2019-01-30T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "604507ba-fd96-4c48-a29a-67a95760d888", - "name": "David Wengier" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "69253", - "title": "Power BI for Developers", - "description": "Integrate, Extend, Embed!\r\n\r\nIn this session, you will learn how developers can deliver real-time dashboards, create custom visuals and embed rich interactive analytics in their apps with Power BI. This presentation specifically targets experienced app developers, and also those curious to understand what developers can achieve with Power BI. Numerous demonstrations will put the theory into action.", - "startsAt": "2019-01-30T15:00:00", - "endsAt": "2019-01-30T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "cc7c7f29-87fc-44b2-a62b-a6ea4a332ea9", - "name": "Peter Myers" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10565, - "name": "AI" - }, - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10573, - "name": "Database" - }, - { - "id": 10586, - "name": "Microsoft" - }, - { - "id": 10596, - "name": "Web" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - }, - { - "id": 2780, - "name": "Room 6", - "session": { - "id": "66459", - "title": "Getting Started with Cosmos DB + EF Core", - "description": "Cosmos DB is great and awesomely fast. Wouldn't be even more amazing if we could use our beloved entity framework to manage it? Let see how we can wire it up and get started", - "startsAt": "2019-01-30T15:00:00", - "endsAt": "2019-01-30T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "c739e2f1-ecf5-43e1-abcf-90cf13dd7b8f", - "name": "Thiago Passos" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10590, - "name": "Serverless" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10573, - "name": "Database" - } - ], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - "index": 5 - } - ] - }, - { - "slotStart": "16:20:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "68804", - "title": "Life Beyond Distributed Transactions: An Apostate's Implementation", - "description": "Over a decade ago, Pat Helland authored his paper, \"Life Beyond Distributed Transactions: An Apostate's Opinion\" describing a means to coordinate activities between entities in databases when a transaction encompassing those entities wasn't feasible or possible. While the paper and subsequent talks provided great insight in the challenges of coordinating activities across entities in a single database, implementations were left as an exercise to the reader!\r\n\r\nFast forward to today, and now we have NoSQL databases, microservices, message queues and brokers, HTTP web services and more that don't (and shouldn't) support any kind of distributed transaction.\r\n\r\nIn this session, we'll look at how to implement coordination between non-transactional resources using Pat's paper as a guide, with examples in Azure Cosmos DB, Azure Service Bus, and Azure SQL Server. We'll look at a real-world example where a codebase assumed everything would be transactional and always succeed, but production proved us wrong! Finally, we'll look at advanced coordination workflows such as Sagas to achieve robust, scalable coordination across the enterprise.", - "startsAt": "2019-01-30T16:20:00", - "endsAt": "2019-01-30T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "f109dd0b-9441-4cbf-8664-19c021a6de4a", - "name": "Jimmy Bogard" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10585, - "name": "Microservices" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "68940", - "title": "Domain-Driven Design: Hidden Lessons from the Big Blue Book", - "description": "We are entering an incredible new era of digital product development where users expect a seamless experience across all of their touchable, wearable, and voice-activated devices. How can we learn to develop software effectively in this new digital-by-default world? \r\n\r\nWhat if the answers are hidden away as secret messages in a 15 year old book? \r\n\r\nAre bounded contexts really used to design loosely coupled code, or are they one of the most powerful organisation design tools used to enable autonomous, self-organising teams? Are core domains just academic jargon that get in the way of shiny technical practices like event sourcing, or is understanding business core domains one of the key differentiators between high-performing delivery teams and the rest of us?\r\n\r\nLet’s go on an adventure and see if the big blue big and can help us in this brave new world.", - "startsAt": "2019-01-30T16:20:00", - "endsAt": "2019-01-30T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "b9d7090c-afe3-4278-ae79-d85987b4aae5", - "name": "Nick Tune" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10566, - "name": "Architecture" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "68493", - "title": "The tech future is diverse", - "description": "By 2020, there will be 4 times more devices connected to the Internet around the world. While technology impacts our everyday life in almost every way, the solutions we create fails to reflect our society or the world we live in. Instead, they often reinforce stereotypes, prejudice, and differences. In this talk, we will look into the lack of diversity and how diversity will make us more suited to solve problems and meet the needs of our society. We will address the culture in our communities, the reasons why minorities quit, and the importance of diversity in tech.", - "startsAt": "2019-01-30T16:20:00", - "endsAt": "2019-01-30T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "50fd0b93-cece-43b5-a7f6-f9ccca500f91", - "name": "Tannaz N. Roshandel" - }, - { - "id": "daafd2d6-0eb2-4eb4-834c-76d51734f6f7", - "name": "Line Moseng" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10578, - "name": "Ethics" - }, - { - "id": 10588, - "name": "People" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "59857", - "title": "CSS Grid - What is this Magic?!", - "description": "We’ve all heard a lot in the last year about a new advancement in the world of CSS, called CSS Grid. Starting off at whispers, we’re now starting to hear it as a deafening roar as more and more developers write about it, talk about it, share it and start using it. In the world of front end, I see it everywhere I turn and am excited as I start to use it in my own projects.\r\n\r\nBut what does this new CSS specification mean for software developers, and why should you care about it? In the world of tech today, we can do so many amazing things and use whatever language we choose across a wide range of devices and platforms. Whether it’s the advent of React and React Native, or frameworks like Electron, it’s easier than ever to build one app that works on multiple platforms with the language we know and work with best. The ability to do this also expands to styling apps on any platform using CSS, and therefore being able to utilise the magical thing that is\r\nCSS Grid.\r\n\r\nThe reason CSS Grid is gaining so much attention, is because it’s a game changer for front end and layouts. With a few simple lines of code, we can now create imaginative, dynamic, responsive layouts (yep, I know that’s a lot of buzz words). While a lot of people are calling this the new ‘table layout’, grid gives us so much more, with the ability to spread cells across columns and rows to whatever size you choose, dictate which direction new items flow, allow cells to move around to fit in place and even tell certain cells exactly where they need to sit.\r\n\r\nWhile there is so much to worry about when developing an app, CSS Grid means that you can worry less about building the layout on the front end, and more about making sure the back end works well. Let me show you how the magic works.", - "startsAt": "2019-01-30T16:20:00", - "endsAt": "2019-01-30T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "d387e75c-ed26-4dc6-8612-0f18abdfd9f5", - "name": "Amy Kapernick" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10596, - "name": "Web" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2780, - "name": "Room 6", - "session": { - "id": "69255", - "title": "Workshop: Embedding Power BI Analytics - Part 1/2", - "description": "This instructor-led workshop focuses on development practices for embedding Power BI reports, dashboards and the Q&A experience, and working with the Power BI JavaScript API.\r\n\r\nThis workshop is designed for web developers experienced with ASP.NET, Visual C#, HTML and JavaScript. You are required to bring your own PC, with Visual Studio 2015 (or later) with web tools installed.", - "startsAt": "2019-01-30T16:20:00", - "endsAt": "2019-01-30T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "cc7c7f29-87fc-44b2-a62b-a6ea4a332ea9", - "name": "Peter Myers" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10596, - "name": "Web" - }, - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10573, - "name": "Database" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10582, - "name": "JavaScript" - }, - { - "id": 10586, - "name": "Microsoft" - } - ], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - "index": 5 - } - ] - }, - { - "slotStart": "17:40:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "69174", - "title": "Hack to the Future", - "description": "Infosec is a continual game of one-upmanship; we build a defence and someone breaks it so we build another one then they break that and the cycle continues. Because of this, the security controls we have at our disposal are rapidly changing and the ones we used yesterday are very often useless today.\r\n\r\nThis talk focuses on what the threats look like *today*. What are we getting wrong, how do we fix it and how do we stay on top in an environment which will be different again tomorrow to what it is today. It's a real-world look at modern defences that everyone building online applications will want to see.", - "startsAt": "2019-01-30T17:40:00", - "endsAt": "2019-01-30T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "735a4b60-42e8-4452-9480-68197372c206", - "name": "Troy Hunt" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10589, - "name": "Security" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "61516", - "title": "Panel discussion on the future of .NET", - "description": "Panel discussion with four experts in the field on the current state of the art and the where .NET and related technologies are heading.\r\n\r\nWe will discuss cross platform development, new features, performance, versioning issues of .NET Core, what’s going to happen with full framework, Blazor, how .NET stands up against competing technologies and where it is all going. \r\n\r\nYou won't cram more info into a session than this, come spend a great hour with us.", - "startsAt": "2019-01-30T17:40:00", - "endsAt": "2019-01-30T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "de972e57-7765-4c38-9dcd-5981587c1433", - "name": "Bryan Hogan" - }, - { - "id": "70590ad0-b1b3-40b8-b05d-b58722ef9d9d", - "name": "Mark Rendle" - }, - { - "id": "cb70e0f2-0f0b-4b8f-b3f4-a9326309a1b2", - "name": "Tess Ferrandez-Norlander" - }, - { - "id": "8b1783d3-15ce-41dc-b989-386759803d97", - "name": "Oren Novotny" - }, - { - "id": "8f64af49-a647-48e7-850b-70505308d948", - "name": "Mads Torgersen" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10596, - "name": "Web" - }, - { - "id": 10586, - "name": "Microsoft" - }, - { - "id": 10572, - "name": "Cross-Platform" - }, - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "62178", - "title": "Patterns for Resilient Architecture", - "description": "We have traditionally built robust software systems by trying to avoid mistakes and by dodging failures when they occur in production or by testing parts of the system in isolation from one another. Modern methods and techniques take a very different approach based on resiliency, which promotes embracing failure instead of trying to avoid it. Resilient architectures enhance observability, leverage well-known patterns such as graceful degradation, timeouts and circuit breakers and embrace chaos engineering, a discipline that promotes breaking things on purpose in order to learn how to build more resilient systems. In this session, will review the most useful patterns for building resilient software systems and I will introduce chaos engineering methodology and especially show the audience how they can benefit from breaking things on purpose.", - "startsAt": "2019-01-30T17:40:00", - "endsAt": "2019-01-30T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "e4fd745a-8a49-43d3-8763-020a4f3512ab", - "name": "Adrian Hornsby" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10585, - "name": "Microservices" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10575, - "name": "DevOps" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "68826", - "title": "A lap around Azure Devops", - "description": "Azure DevOps (previously known as Visual Studio Team Services) is a broad product suite with tools that assists small and large software development teams that want to deliver high quality software at a rapid speed. \r\n\r\nIn session we will walk through all major features in Azure DevOps, such as Azure Boards, Azure Pipelines and Azure Repos, and look at how we can continuously deliver value to or end users and implement DevOps practices such as Infrastructure as Code and testing in production using Azure.", - "startsAt": "2019-01-30T17:40:00", - "endsAt": "2019-01-30T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "54973c7d-2fe5-4a49-9a1c-a52afed4d6a4", - "name": "Jakob Ehn" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10571, - "name": "Continuous Delivery" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10564, - "name": "Agile" - }, - { - "id": 10575, - "name": "DevOps" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "67909", - "title": "Build vs Buy: Software Systems at Jurassic Park", - "description": "We were so preoccupied with whether we could, we didn’t stop to think if we should. Nowhere at Jurassic Park was this more true than how we developed software. Having the wrong software and support structures was a key factor in the failures of our first park. We were entrepreneurs launching something new and architects integrating an enterprise. And our decisions had lasting consequences. Deciding which problems were worth our time was foundational to our failure.\r\n\r\nJoin us for a retrospective of software systems at Jurassic Park. We’ll dig into case studies and explore our successes and failures. We’ll uncover the options, costs, and risks inherent in deciding what software to build, what to buy, and alternatives in between. We’ll explore the opportunity cost of building systems, the sustainability of open-source, and the risks of vendor lock-in. You’ll leave equipped to make better decisions and avoid the pitfalls we made at Jurassic Park.", - "startsAt": "2019-01-30T17:40:00", - "endsAt": "2019-01-30T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "5f017dbb-6821-480b-a750-82a0f15fa1b2", - "name": "Todd Gardner" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10588, - "name": "People" - }, - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10579, - "name": "Fun" - }, - { - "id": 10591, - "name": "Soft Skills" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - } - ] - } - ] - }, - { - "date": "2019-01-31T00:00:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "sessions": [ - { - "id": "69333", - "title": "How We Got Here - The History of Web Development", - "description": "The Internet existed before the Web, but the Web redefined the Internet - what started out as a protocol for helping scientists share documents and references has turned into one of the most important forces in the 21st century. But how did we get here?\r\n\r\nJoin Richard Campbell as he tells the story of the World Wide Web and the web development tools and techniques that made it all possible. From the early versions of HTML where you laid out web pages with tables (GeoCities anyone?) and simple scripting languages to CSS, JavaScript and HTML 5, leading to Single Page Applications, Progressive Web Apps and Web Assembly! We've come a long way, and the story is continuing!", - "startsAt": "2019-01-31T09:00:00", - "endsAt": "2019-01-31T10:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "d4d7850a-526e-4959-aab1-36460081fcf5", - "name": "Richard Campbell" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10596, - "name": "Web" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - { - "id": "69344", - "title": "Structure and Interpretation of Test Cases", - "description": "Throw a line of code into many codebases and it's sure to hit one or more testing frameworks. There's no shortage of frameworks for testing, each with their particular spin and set of conventions, but that glut is not always matched by a clear vision of how to structure and use tests — a framework is a vehicle, but you still need to know how to drive.\r\n\r\nThis talk takes a deep dive into testing, with a strong focus on unit testing, looking at examples and counterexamples in different languages and frameworks, from naming to nesting, exploring the benefits of data-driven testing, the trade-offs between example-based and property-based testing, how to get the most out of the common given–when–then refrain and knowing how far to follow it.", - "startsAt": "2019-01-31T10:20:00", - "endsAt": "2019-01-31T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "bc20c5a9-2d7e-4830-827f-8919be0eba88", - "name": "Kevlin Henney" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10574, - "name": "Design" - }, - { - "id": 10564, - "name": "Agile" - }, - { - "id": 10592, - "name": "Testing" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - { - "id": "68994", - "title": "Beyond Developer", - "description": "When I started in IT the roles were clearly separated. Business Analysts wrote requirements, Architects designed them, Programmers wrote the code, Testers tested the software.\r\n\r\nOver the last decade or so we have seen a shift towards “generalising specialists” who can cut code, understand a business domain, design a user interface, participate in and automate some of the testing and deployment activities, and who are sometimes even responsible for the health and wellbeing of their own systems in production.\r\n\r\nTo succeed in this new world requires more than “3 years of Java”. The modern developer needs to be constantly reinventing themselves, learning, and helping others to do the same. In this session, Dan explores some of the skills and characteristics of the modern developer, and suggests some ways you can grow them for yourself.", - "startsAt": "2019-01-31T11:40:00", - "endsAt": "2019-01-31T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "d06cbb07-240c-4dd9-9c3d-4fd585e084fd", - "name": "Dan North" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10588, - "name": "People" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - { - "id": "69052", - "title": "Blazor, a new framework for browser-based .NET apps", - "description": "Today, nearly all browser-based apps are written in JavaScript (or similar languages that transpile to it). That’s fine, but there’s no good reason to limit our industry to basically one language when so many powerful and mature alternate languages and programming platforms exist. Starting now, WebAssembly opens the floodgates to new choices, and one of the first realistic options may be .NET.\r\n \r\nBlazor is a new experimental web UI framework from the ASP.NET team that aims to brings .NET applications into all browsers (including mobile) via WebAssembly. It allows you to build true full-stack .NET applications, sharing code across server and client, with no need for transpilation or plugins.\r\n \r\nIn this talk I’ll demonstrate what you can do with Blazor today and how it works on the underlying WebAssembly runtime behind the scenes. You’ll see its modern, component-based architecture (inspired by modern SPA frameworks) at work as we use it to build a responsive client-side UI. I’ll cover both basic and advanced scenarios using Blazor’s components, router, DI system, JavaScript interop, and more.", - "startsAt": "2019-01-31T13:40:00", - "endsAt": "2019-01-31T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "05fe7f1a-cc5c-4e6e-8422-281c822c82c3", - "name": "Steve Sanderson" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10582, - "name": "JavaScript" - }, - { - "id": 10596, - "name": "Web" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - { - "id": "69345", - "title": "Where is C# headed?", - "description": "C# 8.0 is coming up! Not just nullable reference types and asynchronous streams, which will get much coverage elsewhere in the conference, but also recursive patterns, switch expressions, ranges, default interface member implementations and more. We’ll look at all of those, and also at some of the things being worked on for future versions of the language.", - "startsAt": "2019-01-31T15:00:00", - "endsAt": "2019-01-31T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "8f64af49-a647-48e7-850b-70505308d948", - "name": "Mads Torgersen" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - { - "id": "66795", - "title": "DiagnosticSourcery 101", - "description": ".NET has a new mechanism for generating and storing diagnostic data: DiagnosticSource. This is the cross-platform alternative to ETW. Much of ASP.NET Core and EF Core produce useful metric data using DiagnosticSource, and you can produce your own and stream some or all of the data to the metrics storage of your choice.\r\n\r\nIn this talk I'll run through how DiagnosticSource works, show you how to use it to output your own metrics in any .NET application, and how to pipe those metrics to a Time-Series database and turn them into a lovely Grafana dashboard.\r\n\r\nYou can use DiagnosticSource in anything from an ASP.NET Core cloud-native microservice to a WPF desktop application, and it's a Microsoft package with no 3rd-party dependencies, so this talk should be interesting and useful for any .NET developer.", - "startsAt": "2019-01-31T16:20:00", - "endsAt": "2019-01-31T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "70590ad0-b1b3-40b8-b05d-b58722ef9d9d", - "name": "Mark Rendle" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10572, - "name": "Cross-Platform" - }, - { - "id": 10575, - "name": "DevOps" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - { - "id": "63671", - "title": "Let's Talk HTTP in .NET Core", - "description": "In the world of microservices (yes, there's that buzzword again!) and distributed systems, we often find ourselves communicating over HTTP. What seems like a simple requirement can quickly become complicated! Networks aren't reliable and services fail. Dealing with those inevitable facts and avoiding a cascading failure can be quite a challenge. In this talk, Steve will explore how we can build .NET Core applications that make HTTP requests and rely on downstream services, whilst remaining resilient and fault tolerant.\r\n\r\nThis session will focus on some of the improvements which have been released in .NET Core and ASP.NET Core 2.1, such as IHttpClientFactory and the new, more performant SocketHttpHandler. Steve will identify some HTTP anti-patterns and common mistakes and demonstrate how we can refactor existing code to use the new HttpClientFactory features.\r\n\r\nNext, Steve will demonstrate other HTTP tips and tricking, including Polly; a fantastic resilience and transient fault handling library which can be used to make your applications less prone to failure. When integrated with the Microsoft IHttpClientFactory; wrapping your HTTP calls in retries, timeouts and circuit-breakers has never been easier!\r\n\r\nIf you're building services which make HTTP calls, then this talk is for you!", - "startsAt": "2019-01-31T17:40:00", - "endsAt": "2019-01-31T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "26b7e78d-aa31-4d7f-b2ea-90fef14d1087", - "name": "Steve Gordon" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10586, - "name": "Microsoft" - }, - { - "id": 10585, - "name": "Microservices" - }, - { - "id": 10596, - "name": "Web" - }, - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10569, - "name": "Cloud" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - } - ], - "hasOnlyPlenumSessions": false - }, - { - "id": 2776, - "name": "Room 2", - "sessions": [ - { - "id": "69266", - "title": "CompSci and My Day Job", - "description": "4 years ago I had a vague idea about Big-O notation and absolutely no clue about combinatorial problems. I knew what a SHA256 hash was (sort of) but I didn't know how it was created, nor that it didn't completely protect some of my data. I knew these things were important, but I never understood how they could apply to the types of applications I was building at the time. All of this changed as I put together the first two volumes of The Imposter's Handbook.\r\n\r\nI get to build a lot of fun things in my new position at Microsoft and I've been surprised at how often I use the things I've learned. Avoiding an obvious performance pitfall with Redis, for instance, because I understood the Big-O implications of the data structure I chose. Going back to ensure that a salt was added to a hash which stored sensitive data for an old client and, most importantly, discouraging a friend from trying to solve a problem that was very clearly NP-Complete.\r\n\r\nIn this talk I'll show you some of the fun things I've learned (like mod(%) and remainder being different things) and how I've applied them to the applications I create for my day job. You might know some of these concepts, or maybe you don't - either way: hopefully you'll leave with a few more tools under your belt to help you do your job better.has grown exponentially over the years, in both market size and developer frustration.\r\n \r\nIn this talk I will walk you through my first few months as an Azure Cloud Developer Engineer, tasked with getting to know Azure, from scratch, while building compelling applications with it. My job is two-fold: I get to show you why Azure is interesting and I then get to tell the Azure product team why it's not. This can be stressful. It can also be quite fun. I'll show you what I've come up with and then you get to decide.", - "startsAt": "2019-01-31T09:00:00", - "endsAt": "2019-01-31T10:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "ecb37172-c6af-49d1-bbb7-3e29ee4b300f", - "name": "Rob Conery" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10586, - "name": "Microsoft" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - { - "id": "68440", - "title": "Leadership Guide for the Reluctant Leader", - "description": "Regardless of the technology you know, regardless of the job title you have, you have amazing potential to impact your workplace, community, and beyond.\r\n\r\nIn this talk, I’ll share a few candid stories of my career failures… I mean… learning opportunities. We’ll start by debunking the myth that leadership == management. Next, we’ll talk about some the attributes, behaviors and skills of good leaders. Last, we’ll cover some practical steps and resources to accelerate your journey.\r\n\r\nYou’ll walk away with some essential leadership skills I believe anyone can develop, and a good dose of encouragement to be more awesome!", - "startsAt": "2019-01-31T10:20:00", - "endsAt": "2019-01-31T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "3884cc4d-8364-4316-9b9a-e16561d87af3", - "name": "David Neal" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10588, - "name": "People" - }, - { - "id": 10591, - "name": "Soft Skills" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - { - "id": "63675", - "title": "The State of C# - What Have I Missed?", - "description": "One of the most popular programming language on the market is getting even better. With every iteration of C# we get more and more features that are meant to make our lives as developers a lot easier. Support for writing (hopefully) better and more readable asynchronous code, being able to do pattern matching, tuples, deconstruction and much more. These are just a few of the many additions to C# that we’ve seen lately.\r\n\r\nJoin me in this session to explore what you’ve missed in one of the most fun to work with programming language on the market; C#!", - "startsAt": "2019-01-31T11:40:00", - "endsAt": "2019-01-31T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "1f8704d7-04db-4f09-9c0e-d5abcbc7c9ff", - "name": "Filip Ekberg" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - { - "id": "69240", - "title": "Small Steps, Giant Leaps: Engineering Lessons from Apollo", - "description": "On July 20th, 1969, Neil Armstrong and Buzz Aldrin became the first humans to set foot on another world. Billions of people tuned in live to watch Apollo 11 land on the moon, but behind Armstrong’s ‘one small step’ lay a decade of astonishing innovation. The Apollo programme wasn’t just about aerospace engineering; it was also responsible for revolutionary new approaches in project management and quality control; new ways of thinking about testing strategies and communications - not to mention delivering a completely bespoke set of hardware and software components that would play a vital role at every stage of the programme.\r\n\r\nAs we celebrate the fiftieth anniversary of the moon landings, let’s take a look back at the technology, processes and practises behind the Apollo programme - and how many of those techniques are still relevant today. What is ‘all-up testing’, and how does it apply to modern software development? Who was the CAPCOM - and what can they teach us about product ownership? How do you manage a distributed team of nearly half a million people? How do you manage scope creep when you’re working to a hard deadline with the whole world watching you? And how DO you fly to the moon and back using a computer with less processing power than an Apple II?", - "startsAt": "2019-01-31T13:40:00", - "endsAt": "2019-01-31T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "1d7dcbfc-1de6-4228-8bd6-04f4ba1c4267", - "name": "Dylan Beattie" - }, - { - "id": "bc20c5a9-2d7e-4830-827f-8919be0eba88", - "name": "Kevlin Henney" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10579, - "name": "Fun" - }, - { - "id": 10588, - "name": "People" - }, - { - "id": 10592, - "name": "Testing" - }, - { - "id": 10574, - "name": "Design" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - { - "id": "68814", - "title": "Scaling microservices with Message queues, .NET and Kubernetes", - "description": "When you design and build applications at scale, you deal with two significant challenges: scalability & robustness. You should design your service so that even if it is subject to intermittent heavy loads, it continues to operate reliably. But how do you build such applications? And how do you deploy an application that scales dynamically? Kubernetes has a feature called autoscaler where instances of your applications are increased or decreased automatically based on metrics that you define.\r\n\r\nIn this talk, you’ll learn how to design, package & deploy reliable .NET applications to Kubernetes & decouple several components using a message broker. You will also learn how to set autoscaling rules to cope with an increasing influx of messages in the queue.", - "startsAt": "2019-01-31T15:00:00", - "endsAt": "2019-01-31T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "964af3ac-fd5e-46f6-b582-6c0d2da30db4", - "name": "Lewis Denham-Parry" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10570, - "name": "Concurrency" - }, - { - "id": 10571, - "name": "Continuous Delivery" - }, - { - "id": 10576, - "name": "Docker" - }, - { - "id": 10575, - "name": "DevOps" - }, - { - "id": 10572, - "name": "Cross-Platform" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10586, - "name": "Microsoft" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - { - "id": "68608", - "title": "A practical guide to deep learning", - "description": "Machine Learning is one of the fastest growing areas of computer science, and Deep Learning (neural networks) is growing even faster, with lots of data and computing power at our fingertips. \r\nThis talk is a practical (very little math) guide to computer vision and deep learning.\r\n\r\nWe will look at a deep learning project from start to finish, look at how to program and train a neural network and gradually refine it using some tips and tricks that you can steal for your future deep learning projects.", - "startsAt": "2019-01-31T16:20:00", - "endsAt": "2019-01-31T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "cb70e0f2-0f0b-4b8f-b3f4-a9326309a1b2", - "name": "Tess Ferrandez-Norlander" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10584, - "name": "Machine Learning" - }, - { - "id": 10565, - "name": "AI" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - { - "id": "69332", - "title": ".NET Rocks Live on Software Feature Selection with Christine Yen", - "description": "Join Carl and Richard from .NET Rocks as they chat with Christine Yen from Honeycomb about how you select features to build in your applications.\r\n\r\nAfter the first version of software is out the door, what do you choose nest? Christine has a background in instrumenting applications to understand what people use – is that the best way to pick features? What about the vision of your own designers? What about asking the users? Bring your questions and come to this live recording of .NET Rocks!", - "startsAt": "2019-01-31T17:40:00", - "endsAt": "2019-01-31T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "d4d7850a-526e-4959-aab1-36460081fcf5", - "name": "Richard Campbell" - }, - { - "id": "bf171bb6-4c78-4fe9-ac77-2338d5ec7975", - "name": "Carl Franklin" - }, - { - "id": "e5d55e8e-93d1-4a53-bbec-10e4beaf5fd8", - "name": "Christine Yen" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - } - ], - "hasOnlyPlenumSessions": false - }, - { - "id": 2777, - "name": "Room 3", - "sessions": [ - { - "id": "68887", - "title": "ASP.NET Core: The One Hour Makeover", - "description": "The “out of the box” template has some lowest common denominator / simplicity tradeoffs that make it easy to understand and work with in a variety of scenarios, but there are lots of performance and deployment tweaks that experienced developers should make before deploying. If you had one hour to tweak a new project, what would you do? I'll include some top open source libraries, best practices from ASP.NET Community Standup links, recommendations from the ASP.NET Core team, etc.", - "startsAt": "2019-01-31T09:00:00", - "endsAt": "2019-01-31T10:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "c6f0ebfe-4c60-421c-b791-1aad49a18005", - "name": "Jon Galloway" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10596, - "name": "Web" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - { - "id": "67965", - "title": "Serverless with Knative", - "description": "When you build a serverless app, you either tie yourself to a cloud provider, or you end up building your own serverless stack. Knative provides a better choice. Knative extends Kubernetes to provide a set of middleware components (build, serving, events) for modern, source-centric, and container-based apps that can run anywhere. In this talk, we’ll see how we can use Knative primitives to build a serverless app that utilizes the Machine Learning magic of the cloud. ", - "startsAt": "2019-01-31T10:20:00", - "endsAt": "2019-01-31T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "f79e1173-a28c-4ad1-8885-7c52ba397fe3", - "name": "Mete Atamel" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10584, - "name": "Machine Learning" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10585, - "name": "Microservices" - }, - { - "id": 10590, - "name": "Serverless" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - { - "id": "67996", - "title": "Functional Web Programming in .Net with the SAFE Stack", - "description": "The SAFE stack is an open source stack of libraries and tools which simplify the process of building type safe web applications which run on the cloud almost entirely in F#. \r\n\r\nIn this talk we'll explore the components of the SAFE stack and how they can be used to write web services and web sites in idiomatic F#. We'll see how we can manage this without needing to compromise and use object oriented frameworks whilst also still integrating with the existing ASP.Net, JavaScript and React ecosystems. We'll consider how we can write backend applications using Saturn on top of ASP.Net, we'll look at how to run F# in the web browser with Fable and we'll cover how we can develop interactive web applications leveraging the benefits of functional programming. This talk is aimed at developers who are looking to understand how they can use F# to effectively build full stack web applications.", - "startsAt": "2019-01-31T11:40:00", - "endsAt": "2019-01-31T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "71cb7875-d2d8-45d1-ae12-7687bae03200", - "name": "Anthony Brown" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10580, - "name": "Functional Programming" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - { - "id": "61211", - "title": "Let’s Talk About Mental Health", - "description": "It’s a great time to be in technology. And yet despite the almost constant improvement in our tools, we somehow don’t spend time talking about how to maintain our most important tool - the one between our ears.\r\n\r\nConstantly feeling worn down, experiencing anxiety over making decisions, and burning out are *not* just facts of a developer’s life! They’re challenges that can be dealt with. In this talk we’ll cover the most common mental health challenges facing developers, and then learn about some techniques to supercharge your brain by improving your mental hygiene (whether you have a psychological disorder or not). Most importantly, you’ll learn how to have a conversation with your coworkers (and other people in your life) about supporting each other and finding your best selves.\r\n", - "startsAt": "2019-01-31T13:40:00", - "endsAt": "2019-01-31T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "fdf3c776-f100-41f5-bb22-7a1f16710740", - "name": "Arthur Doler" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10591, - "name": "Soft Skills" - }, - { - "id": 10588, - "name": "People" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - { - "id": "56795", - "title": "Zero to Mobile Hero - Intro to Xamarin and Visual Studio Team Services", - "description": "You can be faced with a nightmare of Xcode, Android Studio, Swift, Objective C, Swift and other options. This means not only learning multiple languages and frameworks but also having to support two different codebases for the same application. But Xamarin Native and Xamarin.Forms offer a powerful, cross-platform development solution for .NET developers looking to target smartphones, tablets, TV’s, computers and IoT devices.\r\n\r\nIn this talk, Luce shares what Xamarin is including Native and Xamarin.Forms for both C# and F#, how to get started creating a simple HelloWorld app from scratch and a more complex example (will involve at least one Azure service including Cognitive Services for facial recognition). Also how to use Visual Studio Team Services for Continuous Integration and some awesome examples of apps written using Xamarin including ones used to save lives!\r\n\r\nLuce will take examples from xamarin.com/customers as well as show this demo about how Xamarin was used alongside other technologies to aid with Skin Cancer prediction.\r\n\r\nThis talk will include slides, demos, code samples, live coding and the audience will walk away feeling like they too can create a mobile app in just a few minutes and carry their work around with them in their pocket or backpack!", - "startsAt": "2019-01-31T15:00:00", - "endsAt": "2019-01-31T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "79741134-06bb-4596-bc44-40edd9e03f6b", - "name": "Luce Carter" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10565, - "name": "AI" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10572, - "name": "Cross-Platform" - }, - { - "id": 10586, - "name": "Microsoft" - }, - { - "id": 10587, - "name": "Mobile" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - { - "id": "57812", - "title": "Build cross-platform mobile apps using Fabulous", - "description": "In recent years there has been a shift in the way websites and mobile apps are being built - moving to architectures with immutable models and virtual UIs - based on the MVU (model-view-update) pattern. This has lead to great new frameworks like ELM and React for web, and ReactNative for mobile.\r\n\r\nNow there is a new MVU framework for building mobile apps - Fabulous. It's a community-driven open source framework, combining the simplicity of an MVU framework, with 100% native API access for both iOS and Android, all built with using an established, world class, battle-hardened functional programming language.\r\n\r\nThis session will start with an overview of MVU, discussing how it works and why it is such a great architecture. It will then move on to building your first Fabulous app that runs on iOS and Android. Next up more features will be added to the app whilst the app is running on a device, showing the hot reload capabilities of Fabulous for both UI and app logic. Finally it will look at the underlying architecture, see how to use all of the iOS and Android APIs, see how to easily use native components such as cocoa pods or jars, and look at the massive range of libraries that this framework as available to it to do all manner of UI and application logic things. We'll even see how to use it on macOS and Windows, including being able to build iOS apps on Windows (with the help of a networked Mac, Apple licensing rules and whatnot).\r\n\r\nWhen looking at naming for this framework, someone suggested Fabulous. By the end of this session you will see why that name stuck.", - "startsAt": "2019-01-31T16:20:00", - "endsAt": "2019-01-31T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "75465aaf-45f4-4d3c-b78a-2e3d5206b57f", - "name": "Jim Bennett" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10587, - "name": "Mobile" - }, - { - "id": 10586, - "name": "Microsoft" - }, - { - "id": 10580, - "name": "Functional Programming" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - { - "id": "69228", - "title": "Dungeons, Dragons and Functions", - "description": "Dungeons & Dragons, or D&D, is the grand-daddy of all role playing games. While playing D&D is great fun, the rules are a bit daunting for the beginner. The basic rulebook, the PHB, clocks in at a solid 300 pages, and can be extended with multiple additional rule sets. This should come as no surprise to software engineers: this is, after all, documentation for a system that models a complex domain, and has been in use for over 40 years now, going through numerous redesigns over time.\r\n\r\nAs such, D&D rules make for a great exercise in domain modelling. In this talk, I will take you along my journey attempting to tame that monster. We will use no magic, but a weapon imbued with great power, F#; practical tips and tricks for the Adventurer on the functional road will be shared. So... roll 20 for initiative, and join us for an epic adventure in domain modeling with F#!", - "startsAt": "2019-01-31T17:40:00", - "endsAt": "2019-01-31T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "14879952-64d2-42b5-8d6f-d1e1a68b39c9", - "name": "Mathias Brandewinder" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - } - ], - "hasOnlyPlenumSessions": false - }, - { - "id": 2778, - "name": "Room 4", - "sessions": [ - { - "id": "66632", - "title": "A Practical Guide to Dashboarding.", - "description": "Monitoring is difficult. First, there is a vast choice of tooling and setup. Then, figuring out what information should be displayed, where and why can be confusing. Finally what should be alerted upon and how to avoid fatigue.\r\n\r\nTogether we will journey through a practical tour of dashboarding. Focusing on metrics, we will cover how to get information out of your applications using telemetry. I will show you how you might set up your monitoring infrastructure with a demo and talk through some hardened baselines. Finally, I’ll talk through a productionised setup including some gotchas to look out for.\r\n\r\nThis will be a whirlwind tour from start to finish of a practical guide to development dashboarding. ", - "startsAt": "2019-01-31T09:00:00", - "endsAt": "2019-01-31T10:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "c3956212-61f8-4098-94ee-35614895d317", - "name": "Jessica White" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10575, - "name": "DevOps" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - { - "id": "67271", - "title": "Hacking with Go", - "description": "Learning Go programming is easy. Go is popular and becomes even more also in security experts world. Wanted to feel a bit as a hacker? Learn a new language? Or do both at the same time? This session is about it. \r\nSo let's jump into hands-on session and explore how security tools can be written in Go. How to enumerate network resources, extract an information, sniff packets and do port scanning, brute force and more all with Go. \r\nBy the end, you will have more ideas what else can be written or re-written in Go. ", - "startsAt": "2019-01-31T10:20:00", - "endsAt": "2019-01-31T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "4c3efe89-ab58-4b28-a9c3-78251f25ee06", - "name": "Viktorija Almazova" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10589, - "name": "Security" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - { - "id": "57079", - "title": "Augmented Reality - The State of Play", - "description": "Augmented Reality is far more than a Pokémon Go thing now. The hype is real, and many big players (Google, Microsoft, Apple, Facebook, you name it) are pushing AR to become ubiquitous. Hence the abundance of different approaches to AR, a significant need for content creators and creative ways of tackling problems using new techniques.\r\n\r\nIs mobile AR superior to HMDs? What's AR Cloud and why it's important? What are real-world cases solved with AR? Is this all still sci-fi or should you start caring? This session presents the current state of AR, showcases its real capabilities, and demonstrates that we are on the verge of a revolution in how humans interact with digital content.", - "startsAt": "2019-01-31T11:40:00", - "endsAt": "2019-01-31T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "7236f293-a77b-436c-93aa-318f10536a14", - "name": "Rafał Legiędź" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10603, - "name": "VR" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - { - "id": "68736", - "title": "Build Nodejs APIs using Serverless on Azure", - "description": "Serverless lets you focus on coding and testing instead of provisioning infrastructure, configuring web servers, debugging your configuration, managing security settings, and all the drudgery normally associated with getting an app up and running. In this session with, you’ll discover how to migrate an API of an existing app to Azure Functions. You’ll learn how to use Visual Studio Code and the Azure Functions extension to speed up your work. After this session, you’ll join the ranks of serverless developers.", - "startsAt": "2019-01-31T13:40:00", - "endsAt": "2019-01-31T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "047b0c14-4a5f-4f2a-bd84-03b7fef87c4b", - "name": "Simona Cotin" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10582, - "name": "JavaScript" - }, - { - "id": 10590, - "name": "Serverless" - }, - { - "id": 10569, - "name": "Cloud" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - { - "id": "68161", - "title": "ML.NET for developers without any AI experience", - "description": "AI talk is everywhere but where do you start as a .NET developer? During this session, we will explore how you can use AI in the applications your creating today. How to start building & training your ML models with your .NET skills through ML.NET. \r\n\r\nWant to detect laughter in a phone conversation? Detect the mood of Jira tickets or predict if/where your code has bugs. This session will get you started with AI and ML.NET.", - "startsAt": "2019-01-31T15:00:00", - "endsAt": "2019-01-31T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "8061571c-bb36-42a9-b714-4d90c98529fd", - "name": "Lee Mallon" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10565, - "name": "AI" - }, - { - "id": 10584, - "name": "Machine Learning" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - { - "id": "68212", - "title": "Reverse Engineering a Bluetooth Lightbulb", - "description": "I recently made build lights for the company I work for and my home office. They integrate with TeamCity and indicate when a build is running and success/failure of all the tests. In this session, we will reverse engineer a bluetooth light bulb’s protocol, learn how to have an Raspberry Pi communicate with the bulb, and by the end you too will know how to make your own build lights! Please note that this talk will be highly technical. We will be discussing low level details of bluetooth communication, protocol analysis with Wireshark, sniffing bluetooth packets, etc.", - "startsAt": "2019-01-31T16:20:00", - "endsAt": "2019-01-31T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "86d72d85-ef02-405e-b0de-1603dae6b8fb", - "name": "Jesse Phelps" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10606, - "name": "Embedded" - }, - { - "id": 10581, - "name": "IoT" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - { - "id": "64248", - "title": "Data on the Inside, Data on the Outside", - "description": "When we move from a monolith to microservices we abandon integrating via a shared database, as each service must own its own data to allow them it to be autonomous. But now we have a new problem, our data is distributed. What happens if I need one service needs to talk to another about a shared concept such as a product, a hotel room, or an order? Does every service need to have a list of all our users? Who knows what users have permissions to the entities within the micro service? What happens if my REST endpoint needs to include data from a graph that includes other services to make it responsive? And I am not breaking the boundary of my service when all of this data leaves my service boundary in response to a request?\r\n\r\nNaive solutions result in chatty calls as each service engages with multiple other services to fulfil a request, or in large message payloads as services add all the data required to process a message to each message. Neither scale well.\r\n\r\nIn 2005, Pat Helland wrote a paper ‘Data on the Inside vs. Data on the Outside’ which answers the question by distinguishing between data a service owns and reference data that it can use. In this presentation we will explain reference data, how it is classified, and how it should be implemented. We will include a discussion of using reference data from ATOM feeds, discrete messaging and event streams. We’ll provide examples in C#, Python and Go as well as using RMQ and Kafka.", - "startsAt": "2019-01-31T17:40:00", - "endsAt": "2019-01-31T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "84ffe422-5bc6-4075-9903-d9ad3526cf86", - "name": "Ian Cooper" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10585, - "name": "Microservices" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - } - ], - "hasOnlyPlenumSessions": false - }, - { - "id": 2779, - "name": "Room 5", - "sessions": [ - { - "id": "68607", - "title": "Let’s talk patterns", - "description": "At some point in your life, you start realizing that re-inventing the wheel isn't the best way to spend your time. Especially since Bob Wheel Sr. perfected that invention many, many years ago. And it's the same with software. Smart people have already encountered a lot of the problems that we face every day while building software. Some of them have even been nice enough to write down their solutions in so called \"patterns\". So why not stand on the shoulders of...well...maybe not giants...but at least very smart people who were born before you, and build on top of their hard-earned wisdom?\r\nThis session, will walk you through a bunch of really useful patterns, and you'll not only learn their names, but also why they are useful and how to implement them in .NET. And maybe, just maybe, you'll even see that one pattern that solves that problem you are working on at the moment. But even if you don't see that one pattern that you need, you will at least get a few that you can store in your toolbelt for future problems.\r\n", - "startsAt": "2019-01-31T09:00:00", - "endsAt": "2019-01-31T10:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "239cf374-ad4d-4877-a5e5-951cb4cd9291", - "name": "Chris Klug" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - { - "id": "68678", - "title": "Why should you care about edge computing?", - "description": "Recent technologies are enabling a host of new scenarios for doing data collection and analytics much closer to the source, at the \"edge\" so to speak. \r\n\r\nHow can using Artificial Intelligence in a cheap device the size of a matchbox change the way you do things? What kind of scenarios does this open up for business owners, enabling new opportunities for you and your company? What are the actual benefits for connecting the cloud and the edge in this way? \r\n\r\nI'll give you some examples and demonstrate how Edge Computing can enable new scenarios and new business.\r\n", - "startsAt": "2019-01-31T10:20:00", - "endsAt": "2019-01-31T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "fb75b901-4842-4858-ba35-8c370db0af14", - "name": "Glenn F. Henriksen" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10606, - "name": "Embedded" - }, - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10569, - "name": "Cloud" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - { - "id": "67079", - "title": "Keeping it DRYer with Templates", - "description": "One common practice to write maintainable code is to minimise repetition, often referred to as DRY or Don't Repeat Yourself.\r\n\r\nHowever, have you ever considered how much you repeat yourself when you create a new .NET application? \r\n\r\nYou've found your ideal architecture and folder structure and now every time you create a new project you have to create the file structure then add all the different project types you need.\r\n\r\nDon't forget all those NuGet dependencies too and the boilerplate code from other projects you\r\ncontinually copy in. \r\n\r\nIn this talk you'll learn the different ways you can create custom templates for .NET projects using the dotnet CLI, Visual Studio templates and Yeoman, helping to reduce repetition, write better applications, apply incremental improvements, all whilst saving you time and effort. ", - "startsAt": "2019-01-31T11:40:00", - "endsAt": "2019-01-31T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "ac105033-2b20-4b45-8d47-5647d8accf84", - "name": "Layla Porter" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10566, - "name": "Architecture" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - { - "id": "65854", - "title": "Think Like a Trainer: Improving Your Communication Skills", - "description": "Think back to a time when you were in a conversation that could have gone better. Perhaps you said something the wrong way, or you walked away from the conversation not fully knowing if the other person even understood what you were trying to convey.\r\n\r\nTechnical trainers rely on effective communication as the foundation of everything that we do. We help end users to learn how to use software and adjust to new workflows, through the process of constantly adapting to different backgrounds, skill levels, and learning styles.\r\n\r\nIn this session, you’ll learn actionable strategies to begin thinking like a trainer, including:\r\n\r\n- Using active listening techniques to communicate with empathy.\r\n\r\n- Best practices for explaining technical concepts in non-technical terms.\r\n\r\n- Adjusting your communication approach for different communication styles.\r\n\r\n- Using problem solving skills to help you get unstuck during difficult conversations.", - "startsAt": "2019-01-31T13:40:00", - "endsAt": "2019-01-31T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "25cc75bc-df7b-42ad-a3e4-e0bfa8af7f01", - "name": "Olivia Liddell" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10588, - "name": "People" - }, - { - "id": 10591, - "name": "Soft Skills" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - { - "id": "66437", - "title": "(WPF + WinForms) * .NET Core = Modern Desktop", - "description": "Learn how .NET Core 3 brings WPF and Windows Forms into the future with a modern runtime. See what’s new for WPF and Windows Forms, learn how to easily retarget your .NET Framework application over to .NET Core, and how to get these modern desktop apps to your users.\r\n", - "startsAt": "2019-01-31T15:00:00", - "endsAt": "2019-01-31T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "8b1783d3-15ce-41dc-b989-386759803d97", - "name": "Oren Novotny" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10594, - "name": "UI" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - { - "id": "66866", - "title": "An Introduction to WebAssembly", - "description": "Want to write a web application? Better get familiar with JavaScript JavaScript has long been the king of front-end. While there have been various attempts to dethrone it, they have typically involved treating JavaScript as an assembly-language analog that you transpile your code to. This has lead to complex build pipelines that result in JavaScript which the browser has to parse and *you* still have to debug. But what if there were an actual byte-code language you could compile your non-JavaScript code to instead? That is what WebAssembly is.\r\n\r\nI'm going to explain how WebAssembly works and how to use it in this talk. I'll cover what it is, how it fits into your application, and how to build and use your own WebAssembly modules. And, I'll demo how to build and use those modules with both Rust and the WebAssembly Text Format. That's right, I'll be live coding in an assembly language. I'll also go over some online resources for other languages and tools that make use of WebAssembly.\r\n\r\nWhen we're done, you'll have the footing you need to start building applications featuring WebAssembly. So grab a non-JavaScript language, a modern browser, and let's and get started!", - "startsAt": "2019-01-31T16:20:00", - "endsAt": "2019-01-31T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "520bf4ca-b2d3-47c3-8475-c25bb2b257f7", - "name": "Guy Royse" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10596, - "name": "Web" - }, - { - "id": 10582, - "name": "JavaScript" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - { - "id": "68009", - "title": "Scaling Frontend Development", - "description": "Abstract\r\nLarge frontend projects suffer from all of the same problems of other software projects with the same characteristics:\r\nCommunication and coordination overhead\r\nSpeed of delivery\r\nLarge quantum size\r\nLow productivity \r\n\r\nThese factors are growing concerns for companies as productivity and speed of delivery is nowadays crucial for success in this competitive landscape\r\n\r\nA common way of solving many of these issues in software projects is the microservices approach.\r\n\r\nToday a similar approach of applying this same pattern to the frontend is being popularized and is commonly nominated as micro-frontends.\r\n\r\nHowever this approach has many fallacies because of frontend specific characteristics:\r\n- The performance cost of loading many independent applications\r\n- The need for a common visual language and experience\r\n- The browser is a monolithic runtime \r\n\r\nDescription\r\nThis talk will be about the problems of large frontend projects and how to scale them in a way that balances team autonomy with performance and productivity\r\n\r\nI’ll talk about how at farfetch we’re progressively refactoring our frontend to multiple independent applications, and the strategies we’re using to make sure that despite having many applications made by different independent and geographically distributed teams we still can deliver a performant and consistent product to the end user.\r\n", - "startsAt": "2019-01-31T17:40:00", - "endsAt": "2019-01-31T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "f79c1f81-b367-4ce0-9cc0-4263f30a9f7f", - "name": "luis vieira" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10594, - "name": "UI" - }, - { - "id": 10585, - "name": "Microservices" - }, - { - "id": 10582, - "name": "JavaScript" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - } - ], - "hasOnlyPlenumSessions": false - }, - { - "id": 2780, - "name": "Room 6", - "sessions": [ - { - "id": "69354", - "title": "Deep Learning with PyTorch", - "description": "Deep Learning is fast becoming an indispensable approach to getting the most from your data. In this session attendees will learn both how Deep Learning fits into the Artificial Intelligence landscape as well as how to get started using PyTorch. The session will start with the basic intuitions behind the problem setup, models, and optimization methods used to solve computer vision problems.\r\n\r\nAttendees should come away with a strong foundation of how to both create deep learning models as well as how to consume them in their applications. Prior exposure to PyTorch is definitely not expected.", - "startsAt": "2019-01-31T10:20:00", - "endsAt": "2019-01-31T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "1825cfcf-5ad4-4ab2-94cb-1de5735cac83", - "name": "Seth Juarez" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - { - "id": "59563", - "title": "Skip the first three months of development for your next app", - "description": "Five amazing ways using the emerging cloud ecosystem can save you time. The true power of serverless comes from using not just the execution service, but the whole platform around it. This talk/demo shows how teams can use AWS Lambda with Cognito, IOT, Kinesis and S3 to achieve in a few hours what usually takes the the first three months of application development.", - "startsAt": "2019-01-31T11:40:00", - "endsAt": "2019-01-31T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "1eb07015-2101-4cf7-91b9-f255752af538", - "name": "Gojko Adzic" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10571, - "name": "Continuous Delivery" - }, - { - "id": 10590, - "name": "Serverless" - }, - { - "id": 10585, - "name": "Microservices" - }, - { - "id": 10581, - "name": "IoT" - }, - { - "id": 10575, - "name": "DevOps" - } - ], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - { - "id": "66322", - "title": "Accessible App Design", - "description": "Want to start building apps for people with disabilities but doesn’t no where to start? In this session we cover everything to start right now! There are many people with learning disabilities around the world. There are also people with autism, psychological disabilities… Many of them are also entering the era of mobile and internet. We talk about design guidelines that can reach them. \r\n\r\nWe talk about apps, but do not focus on a specific technology in this talk. Which apps are needed? Which steps of design helps them to catch the frontiers of mobile and internet right now? Microsoft and other big tech companies are working more and more about inclusive design and digital inclusion for people with disabilities. ", - "startsAt": "2019-01-31T15:00:00", - "endsAt": "2019-01-31T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "67a075a3-a111-4116-bd44-a25a01485959", - "name": "Dennie Declercq" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10591, - "name": "Soft Skills" - }, - { - "id": 10594, - "name": "UI" - }, - { - "id": 10595, - "name": "UX" - } - ], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - { - "id": "58551", - "title": "System Stable : Robust connected applications with Polly, the .NET Resilience Framework", - "description": "Short Description\r\n\r\nJoin me for this session and I will show you how with just a few lines of code, you can make your applications much more resilient and reliable. With Polly, the .NET resilience framework, your application can easily tolerate transient faults and longer outages in remote systems or infrastructure. \r\n\r\nAt the end of this hour you will know how to use all the features of Polly to make your application a rock solid piece of work. We’ll cover the reactive and proactive resilience strategies, starting with simple but very powerful retries and finishing with bulkhead isolation.\r\nOh, and did I mention Polly is a .NET Standard library so it will work on any application you can think of! Join me for an hour, and your applications will never be the same.\r\n\r\n----------------------------------------------------------------------------------------------------------------------------------\r\n\r\nFull Description\r\n\r\nJoin me for this session and I will show you how with just a few lines of code, you can make your applications much more resilient and reliable. Let me tell you more… \r\n\r\nAlmost all applications now depend on connectivity, but what do you do when the infrastructure is unreliable or the remote system is down or returns an error? Does your application grind to a halt or just drop that single request? What if you could recover from these kinds of error, maybe even so quickly it won’t be noticed? \r\n \r\nWith Polly your application can easily tolerate transient faults and longer outages in remote systems or infrastructure. \r\n\r\nAt the end of this hour you will know how to use all the features of Polly to make your application a rock solid piece of work. \r\n\r\nWe’ll start with the simple but very powerful Retry Policy which lets you retry a failed request. If simple retries are not good enough for you, there is a Wait and Retry policy which introduces a delay between retries, giving the remote service time to recover before being hit again. Then I show you how to use the circuit breaker for when things have really gone wrong and a remote system is struggling under too much load or has failed. If all these attempts are unsuccessful and you are still not getting through to the remote system, you can return a default response or execute arbitrary code to call for human help (or restart the cloud) with the fallback policy. \r\n\r\nThat takes care of what you can do when things go wrong, but Polly also lets you take proactive steps to keep your application and the services it depends on healthy. \r\n\r\nTo get you started with proactive strategies, you will learn how caching can be used to store and return responses from an in-memory or distributed cache without having to hit the remote server every time. Or you can use bulkhead isolation to marshal resources within your application so that no one struggling part can take down the whole. Finally, I’ll show you how to fail quickly when your application is in danger of being overloaded or the remote systems is not responding in a timely fashion, this is done with the bulkhead isolation and the timeout polices. \r\n\r\nOh, and did I mention Polly is a .NET Standard library so it will work on any application you can think of! Join me for an hour, and your applications will never be the same. ", - "startsAt": "2019-01-31T16:20:00", - "endsAt": "2019-01-31T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "de972e57-7765-4c38-9dcd-5981587c1433", - "name": "Bryan Hogan" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10596, - "name": "Web" - }, - { - "id": 10587, - "name": "Mobile" - }, - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - { - "id": "68611", - "title": "Clean Architecture with ASP.NET Core 2.2", - "description": "The explosive growth of web frameworks and the demands of users have changed the approach to building web applications. Many challenges exist, and getting started can be a daunting prospect. Let's change that now.\r\n\r\nThis talk provides practical guidance and recommendations. We will cover architecture, technologies, tools, and frameworks. We will examine strategies for organizing your projects, folders and files. We will design a system that is simple to build and maintain - all the way from development to production. You leave this talk inspired and prepared to take your enterprise application development to the next level.", - "startsAt": "2019-01-31T17:40:00", - "endsAt": "2019-01-31T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "3a5abb12-c9e2-4641-8ace-a4c067ad8e8d", - "name": "Jason Taylor" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - } - ], - "hasOnlyPlenumSessions": false - } - ], - "timeSlots": [ - { - "slotStart": "09:00:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "69333", - "title": "How We Got Here - The History of Web Development", - "description": "The Internet existed before the Web, but the Web redefined the Internet - what started out as a protocol for helping scientists share documents and references has turned into one of the most important forces in the 21st century. But how did we get here?\r\n\r\nJoin Richard Campbell as he tells the story of the World Wide Web and the web development tools and techniques that made it all possible. From the early versions of HTML where you laid out web pages with tables (GeoCities anyone?) and simple scripting languages to CSS, JavaScript and HTML 5, leading to Single Page Applications, Progressive Web Apps and Web Assembly! We've come a long way, and the story is continuing!", - "startsAt": "2019-01-31T09:00:00", - "endsAt": "2019-01-31T10:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "d4d7850a-526e-4959-aab1-36460081fcf5", - "name": "Richard Campbell" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10596, - "name": "Web" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "69266", - "title": "CompSci and My Day Job", - "description": "4 years ago I had a vague idea about Big-O notation and absolutely no clue about combinatorial problems. I knew what a SHA256 hash was (sort of) but I didn't know how it was created, nor that it didn't completely protect some of my data. I knew these things were important, but I never understood how they could apply to the types of applications I was building at the time. All of this changed as I put together the first two volumes of The Imposter's Handbook.\r\n\r\nI get to build a lot of fun things in my new position at Microsoft and I've been surprised at how often I use the things I've learned. Avoiding an obvious performance pitfall with Redis, for instance, because I understood the Big-O implications of the data structure I chose. Going back to ensure that a salt was added to a hash which stored sensitive data for an old client and, most importantly, discouraging a friend from trying to solve a problem that was very clearly NP-Complete.\r\n\r\nIn this talk I'll show you some of the fun things I've learned (like mod(%) and remainder being different things) and how I've applied them to the applications I create for my day job. You might know some of these concepts, or maybe you don't - either way: hopefully you'll leave with a few more tools under your belt to help you do your job better.has grown exponentially over the years, in both market size and developer frustration.\r\n \r\nIn this talk I will walk you through my first few months as an Azure Cloud Developer Engineer, tasked with getting to know Azure, from scratch, while building compelling applications with it. My job is two-fold: I get to show you why Azure is interesting and I then get to tell the Azure product team why it's not. This can be stressful. It can also be quite fun. I'll show you what I've come up with and then you get to decide.", - "startsAt": "2019-01-31T09:00:00", - "endsAt": "2019-01-31T10:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "ecb37172-c6af-49d1-bbb7-3e29ee4b300f", - "name": "Rob Conery" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10586, - "name": "Microsoft" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "68887", - "title": "ASP.NET Core: The One Hour Makeover", - "description": "The “out of the box” template has some lowest common denominator / simplicity tradeoffs that make it easy to understand and work with in a variety of scenarios, but there are lots of performance and deployment tweaks that experienced developers should make before deploying. If you had one hour to tweak a new project, what would you do? I'll include some top open source libraries, best practices from ASP.NET Community Standup links, recommendations from the ASP.NET Core team, etc.", - "startsAt": "2019-01-31T09:00:00", - "endsAt": "2019-01-31T10:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "c6f0ebfe-4c60-421c-b791-1aad49a18005", - "name": "Jon Galloway" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10596, - "name": "Web" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "66632", - "title": "A Practical Guide to Dashboarding.", - "description": "Monitoring is difficult. First, there is a vast choice of tooling and setup. Then, figuring out what information should be displayed, where and why can be confusing. Finally what should be alerted upon and how to avoid fatigue.\r\n\r\nTogether we will journey through a practical tour of dashboarding. Focusing on metrics, we will cover how to get information out of your applications using telemetry. I will show you how you might set up your monitoring infrastructure with a demo and talk through some hardened baselines. Finally, I’ll talk through a productionised setup including some gotchas to look out for.\r\n\r\nThis will be a whirlwind tour from start to finish of a practical guide to development dashboarding. ", - "startsAt": "2019-01-31T09:00:00", - "endsAt": "2019-01-31T10:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "c3956212-61f8-4098-94ee-35614895d317", - "name": "Jessica White" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10575, - "name": "DevOps" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "68607", - "title": "Let’s talk patterns", - "description": "At some point in your life, you start realizing that re-inventing the wheel isn't the best way to spend your time. Especially since Bob Wheel Sr. perfected that invention many, many years ago. And it's the same with software. Smart people have already encountered a lot of the problems that we face every day while building software. Some of them have even been nice enough to write down their solutions in so called \"patterns\". So why not stand on the shoulders of...well...maybe not giants...but at least very smart people who were born before you, and build on top of their hard-earned wisdom?\r\nThis session, will walk you through a bunch of really useful patterns, and you'll not only learn their names, but also why they are useful and how to implement them in .NET. And maybe, just maybe, you'll even see that one pattern that solves that problem you are working on at the moment. But even if you don't see that one pattern that you need, you will at least get a few that you can store in your toolbelt for future problems.\r\n", - "startsAt": "2019-01-31T09:00:00", - "endsAt": "2019-01-31T10:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "239cf374-ad4d-4877-a5e5-951cb4cd9291", - "name": "Chris Klug" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - } - ] - }, - { - "slotStart": "10:20:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "69344", - "title": "Structure and Interpretation of Test Cases", - "description": "Throw a line of code into many codebases and it's sure to hit one or more testing frameworks. There's no shortage of frameworks for testing, each with their particular spin and set of conventions, but that glut is not always matched by a clear vision of how to structure and use tests — a framework is a vehicle, but you still need to know how to drive.\r\n\r\nThis talk takes a deep dive into testing, with a strong focus on unit testing, looking at examples and counterexamples in different languages and frameworks, from naming to nesting, exploring the benefits of data-driven testing, the trade-offs between example-based and property-based testing, how to get the most out of the common given–when–then refrain and knowing how far to follow it.", - "startsAt": "2019-01-31T10:20:00", - "endsAt": "2019-01-31T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "bc20c5a9-2d7e-4830-827f-8919be0eba88", - "name": "Kevlin Henney" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10574, - "name": "Design" - }, - { - "id": 10564, - "name": "Agile" - }, - { - "id": 10592, - "name": "Testing" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "68440", - "title": "Leadership Guide for the Reluctant Leader", - "description": "Regardless of the technology you know, regardless of the job title you have, you have amazing potential to impact your workplace, community, and beyond.\r\n\r\nIn this talk, I’ll share a few candid stories of my career failures… I mean… learning opportunities. We’ll start by debunking the myth that leadership == management. Next, we’ll talk about some the attributes, behaviors and skills of good leaders. Last, we’ll cover some practical steps and resources to accelerate your journey.\r\n\r\nYou’ll walk away with some essential leadership skills I believe anyone can develop, and a good dose of encouragement to be more awesome!", - "startsAt": "2019-01-31T10:20:00", - "endsAt": "2019-01-31T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "3884cc4d-8364-4316-9b9a-e16561d87af3", - "name": "David Neal" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10588, - "name": "People" - }, - { - "id": 10591, - "name": "Soft Skills" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "67965", - "title": "Serverless with Knative", - "description": "When you build a serverless app, you either tie yourself to a cloud provider, or you end up building your own serverless stack. Knative provides a better choice. Knative extends Kubernetes to provide a set of middleware components (build, serving, events) for modern, source-centric, and container-based apps that can run anywhere. In this talk, we’ll see how we can use Knative primitives to build a serverless app that utilizes the Machine Learning magic of the cloud. ", - "startsAt": "2019-01-31T10:20:00", - "endsAt": "2019-01-31T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "f79e1173-a28c-4ad1-8885-7c52ba397fe3", - "name": "Mete Atamel" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10584, - "name": "Machine Learning" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10585, - "name": "Microservices" - }, - { - "id": 10590, - "name": "Serverless" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "67271", - "title": "Hacking with Go", - "description": "Learning Go programming is easy. Go is popular and becomes even more also in security experts world. Wanted to feel a bit as a hacker? Learn a new language? Or do both at the same time? This session is about it. \r\nSo let's jump into hands-on session and explore how security tools can be written in Go. How to enumerate network resources, extract an information, sniff packets and do port scanning, brute force and more all with Go. \r\nBy the end, you will have more ideas what else can be written or re-written in Go. ", - "startsAt": "2019-01-31T10:20:00", - "endsAt": "2019-01-31T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "4c3efe89-ab58-4b28-a9c3-78251f25ee06", - "name": "Viktorija Almazova" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10589, - "name": "Security" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "68678", - "title": "Why should you care about edge computing?", - "description": "Recent technologies are enabling a host of new scenarios for doing data collection and analytics much closer to the source, at the \"edge\" so to speak. \r\n\r\nHow can using Artificial Intelligence in a cheap device the size of a matchbox change the way you do things? What kind of scenarios does this open up for business owners, enabling new opportunities for you and your company? What are the actual benefits for connecting the cloud and the edge in this way? \r\n\r\nI'll give you some examples and demonstrate how Edge Computing can enable new scenarios and new business.\r\n", - "startsAt": "2019-01-31T10:20:00", - "endsAt": "2019-01-31T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "fb75b901-4842-4858-ba35-8c370db0af14", - "name": "Glenn F. Henriksen" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10606, - "name": "Embedded" - }, - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10569, - "name": "Cloud" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - }, - { - "id": 2780, - "name": "Room 6", - "session": { - "id": "69354", - "title": "Deep Learning with PyTorch", - "description": "Deep Learning is fast becoming an indispensable approach to getting the most from your data. In this session attendees will learn both how Deep Learning fits into the Artificial Intelligence landscape as well as how to get started using PyTorch. The session will start with the basic intuitions behind the problem setup, models, and optimization methods used to solve computer vision problems.\r\n\r\nAttendees should come away with a strong foundation of how to both create deep learning models as well as how to consume them in their applications. Prior exposure to PyTorch is definitely not expected.", - "startsAt": "2019-01-31T10:20:00", - "endsAt": "2019-01-31T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "1825cfcf-5ad4-4ab2-94cb-1de5735cac83", - "name": "Seth Juarez" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - "index": 5 - } - ] - }, - { - "slotStart": "11:40:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "68994", - "title": "Beyond Developer", - "description": "When I started in IT the roles were clearly separated. Business Analysts wrote requirements, Architects designed them, Programmers wrote the code, Testers tested the software.\r\n\r\nOver the last decade or so we have seen a shift towards “generalising specialists” who can cut code, understand a business domain, design a user interface, participate in and automate some of the testing and deployment activities, and who are sometimes even responsible for the health and wellbeing of their own systems in production.\r\n\r\nTo succeed in this new world requires more than “3 years of Java”. The modern developer needs to be constantly reinventing themselves, learning, and helping others to do the same. In this session, Dan explores some of the skills and characteristics of the modern developer, and suggests some ways you can grow them for yourself.", - "startsAt": "2019-01-31T11:40:00", - "endsAt": "2019-01-31T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "d06cbb07-240c-4dd9-9c3d-4fd585e084fd", - "name": "Dan North" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10588, - "name": "People" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "63675", - "title": "The State of C# - What Have I Missed?", - "description": "One of the most popular programming language on the market is getting even better. With every iteration of C# we get more and more features that are meant to make our lives as developers a lot easier. Support for writing (hopefully) better and more readable asynchronous code, being able to do pattern matching, tuples, deconstruction and much more. These are just a few of the many additions to C# that we’ve seen lately.\r\n\r\nJoin me in this session to explore what you’ve missed in one of the most fun to work with programming language on the market; C#!", - "startsAt": "2019-01-31T11:40:00", - "endsAt": "2019-01-31T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "1f8704d7-04db-4f09-9c0e-d5abcbc7c9ff", - "name": "Filip Ekberg" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "67996", - "title": "Functional Web Programming in .Net with the SAFE Stack", - "description": "The SAFE stack is an open source stack of libraries and tools which simplify the process of building type safe web applications which run on the cloud almost entirely in F#. \r\n\r\nIn this talk we'll explore the components of the SAFE stack and how they can be used to write web services and web sites in idiomatic F#. We'll see how we can manage this without needing to compromise and use object oriented frameworks whilst also still integrating with the existing ASP.Net, JavaScript and React ecosystems. We'll consider how we can write backend applications using Saturn on top of ASP.Net, we'll look at how to run F# in the web browser with Fable and we'll cover how we can develop interactive web applications leveraging the benefits of functional programming. This talk is aimed at developers who are looking to understand how they can use F# to effectively build full stack web applications.", - "startsAt": "2019-01-31T11:40:00", - "endsAt": "2019-01-31T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "71cb7875-d2d8-45d1-ae12-7687bae03200", - "name": "Anthony Brown" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10580, - "name": "Functional Programming" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "57079", - "title": "Augmented Reality - The State of Play", - "description": "Augmented Reality is far more than a Pokémon Go thing now. The hype is real, and many big players (Google, Microsoft, Apple, Facebook, you name it) are pushing AR to become ubiquitous. Hence the abundance of different approaches to AR, a significant need for content creators and creative ways of tackling problems using new techniques.\r\n\r\nIs mobile AR superior to HMDs? What's AR Cloud and why it's important? What are real-world cases solved with AR? Is this all still sci-fi or should you start caring? This session presents the current state of AR, showcases its real capabilities, and demonstrates that we are on the verge of a revolution in how humans interact with digital content.", - "startsAt": "2019-01-31T11:40:00", - "endsAt": "2019-01-31T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "7236f293-a77b-436c-93aa-318f10536a14", - "name": "Rafał Legiędź" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10603, - "name": "VR" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "67079", - "title": "Keeping it DRYer with Templates", - "description": "One common practice to write maintainable code is to minimise repetition, often referred to as DRY or Don't Repeat Yourself.\r\n\r\nHowever, have you ever considered how much you repeat yourself when you create a new .NET application? \r\n\r\nYou've found your ideal architecture and folder structure and now every time you create a new project you have to create the file structure then add all the different project types you need.\r\n\r\nDon't forget all those NuGet dependencies too and the boilerplate code from other projects you\r\ncontinually copy in. \r\n\r\nIn this talk you'll learn the different ways you can create custom templates for .NET projects using the dotnet CLI, Visual Studio templates and Yeoman, helping to reduce repetition, write better applications, apply incremental improvements, all whilst saving you time and effort. ", - "startsAt": "2019-01-31T11:40:00", - "endsAt": "2019-01-31T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "ac105033-2b20-4b45-8d47-5647d8accf84", - "name": "Layla Porter" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10566, - "name": "Architecture" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - }, - { - "id": 2780, - "name": "Room 6", - "session": { - "id": "59563", - "title": "Skip the first three months of development for your next app", - "description": "Five amazing ways using the emerging cloud ecosystem can save you time. The true power of serverless comes from using not just the execution service, but the whole platform around it. This talk/demo shows how teams can use AWS Lambda with Cognito, IOT, Kinesis and S3 to achieve in a few hours what usually takes the the first three months of application development.", - "startsAt": "2019-01-31T11:40:00", - "endsAt": "2019-01-31T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "1eb07015-2101-4cf7-91b9-f255752af538", - "name": "Gojko Adzic" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10571, - "name": "Continuous Delivery" - }, - { - "id": 10590, - "name": "Serverless" - }, - { - "id": 10585, - "name": "Microservices" - }, - { - "id": 10581, - "name": "IoT" - }, - { - "id": 10575, - "name": "DevOps" - } - ], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - "index": 5 - } - ] - }, - { - "slotStart": "13:40:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "69052", - "title": "Blazor, a new framework for browser-based .NET apps", - "description": "Today, nearly all browser-based apps are written in JavaScript (or similar languages that transpile to it). That’s fine, but there’s no good reason to limit our industry to basically one language when so many powerful and mature alternate languages and programming platforms exist. Starting now, WebAssembly opens the floodgates to new choices, and one of the first realistic options may be .NET.\r\n \r\nBlazor is a new experimental web UI framework from the ASP.NET team that aims to brings .NET applications into all browsers (including mobile) via WebAssembly. It allows you to build true full-stack .NET applications, sharing code across server and client, with no need for transpilation or plugins.\r\n \r\nIn this talk I’ll demonstrate what you can do with Blazor today and how it works on the underlying WebAssembly runtime behind the scenes. You’ll see its modern, component-based architecture (inspired by modern SPA frameworks) at work as we use it to build a responsive client-side UI. I’ll cover both basic and advanced scenarios using Blazor’s components, router, DI system, JavaScript interop, and more.", - "startsAt": "2019-01-31T13:40:00", - "endsAt": "2019-01-31T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "05fe7f1a-cc5c-4e6e-8422-281c822c82c3", - "name": "Steve Sanderson" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10582, - "name": "JavaScript" - }, - { - "id": 10596, - "name": "Web" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "69240", - "title": "Small Steps, Giant Leaps: Engineering Lessons from Apollo", - "description": "On July 20th, 1969, Neil Armstrong and Buzz Aldrin became the first humans to set foot on another world. Billions of people tuned in live to watch Apollo 11 land on the moon, but behind Armstrong’s ‘one small step’ lay a decade of astonishing innovation. The Apollo programme wasn’t just about aerospace engineering; it was also responsible for revolutionary new approaches in project management and quality control; new ways of thinking about testing strategies and communications - not to mention delivering a completely bespoke set of hardware and software components that would play a vital role at every stage of the programme.\r\n\r\nAs we celebrate the fiftieth anniversary of the moon landings, let’s take a look back at the technology, processes and practises behind the Apollo programme - and how many of those techniques are still relevant today. What is ‘all-up testing’, and how does it apply to modern software development? Who was the CAPCOM - and what can they teach us about product ownership? How do you manage a distributed team of nearly half a million people? How do you manage scope creep when you’re working to a hard deadline with the whole world watching you? And how DO you fly to the moon and back using a computer with less processing power than an Apple II?", - "startsAt": "2019-01-31T13:40:00", - "endsAt": "2019-01-31T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "1d7dcbfc-1de6-4228-8bd6-04f4ba1c4267", - "name": "Dylan Beattie" - }, - { - "id": "bc20c5a9-2d7e-4830-827f-8919be0eba88", - "name": "Kevlin Henney" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10579, - "name": "Fun" - }, - { - "id": 10588, - "name": "People" - }, - { - "id": 10592, - "name": "Testing" - }, - { - "id": 10574, - "name": "Design" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "61211", - "title": "Let’s Talk About Mental Health", - "description": "It’s a great time to be in technology. And yet despite the almost constant improvement in our tools, we somehow don’t spend time talking about how to maintain our most important tool - the one between our ears.\r\n\r\nConstantly feeling worn down, experiencing anxiety over making decisions, and burning out are *not* just facts of a developer’s life! They’re challenges that can be dealt with. In this talk we’ll cover the most common mental health challenges facing developers, and then learn about some techniques to supercharge your brain by improving your mental hygiene (whether you have a psychological disorder or not). Most importantly, you’ll learn how to have a conversation with your coworkers (and other people in your life) about supporting each other and finding your best selves.\r\n", - "startsAt": "2019-01-31T13:40:00", - "endsAt": "2019-01-31T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "fdf3c776-f100-41f5-bb22-7a1f16710740", - "name": "Arthur Doler" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10591, - "name": "Soft Skills" - }, - { - "id": 10588, - "name": "People" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "68736", - "title": "Build Nodejs APIs using Serverless on Azure", - "description": "Serverless lets you focus on coding and testing instead of provisioning infrastructure, configuring web servers, debugging your configuration, managing security settings, and all the drudgery normally associated with getting an app up and running. In this session with, you’ll discover how to migrate an API of an existing app to Azure Functions. You’ll learn how to use Visual Studio Code and the Azure Functions extension to speed up your work. After this session, you’ll join the ranks of serverless developers.", - "startsAt": "2019-01-31T13:40:00", - "endsAt": "2019-01-31T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "047b0c14-4a5f-4f2a-bd84-03b7fef87c4b", - "name": "Simona Cotin" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10582, - "name": "JavaScript" - }, - { - "id": 10590, - "name": "Serverless" - }, - { - "id": 10569, - "name": "Cloud" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "65854", - "title": "Think Like a Trainer: Improving Your Communication Skills", - "description": "Think back to a time when you were in a conversation that could have gone better. Perhaps you said something the wrong way, or you walked away from the conversation not fully knowing if the other person even understood what you were trying to convey.\r\n\r\nTechnical trainers rely on effective communication as the foundation of everything that we do. We help end users to learn how to use software and adjust to new workflows, through the process of constantly adapting to different backgrounds, skill levels, and learning styles.\r\n\r\nIn this session, you’ll learn actionable strategies to begin thinking like a trainer, including:\r\n\r\n- Using active listening techniques to communicate with empathy.\r\n\r\n- Best practices for explaining technical concepts in non-technical terms.\r\n\r\n- Adjusting your communication approach for different communication styles.\r\n\r\n- Using problem solving skills to help you get unstuck during difficult conversations.", - "startsAt": "2019-01-31T13:40:00", - "endsAt": "2019-01-31T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "25cc75bc-df7b-42ad-a3e4-e0bfa8af7f01", - "name": "Olivia Liddell" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10588, - "name": "People" - }, - { - "id": 10591, - "name": "Soft Skills" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - } - ] - }, - { - "slotStart": "15:00:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "69345", - "title": "Where is C# headed?", - "description": "C# 8.0 is coming up! Not just nullable reference types and asynchronous streams, which will get much coverage elsewhere in the conference, but also recursive patterns, switch expressions, ranges, default interface member implementations and more. We’ll look at all of those, and also at some of the things being worked on for future versions of the language.", - "startsAt": "2019-01-31T15:00:00", - "endsAt": "2019-01-31T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "8f64af49-a647-48e7-850b-70505308d948", - "name": "Mads Torgersen" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "68814", - "title": "Scaling microservices with Message queues, .NET and Kubernetes", - "description": "When you design and build applications at scale, you deal with two significant challenges: scalability & robustness. You should design your service so that even if it is subject to intermittent heavy loads, it continues to operate reliably. But how do you build such applications? And how do you deploy an application that scales dynamically? Kubernetes has a feature called autoscaler where instances of your applications are increased or decreased automatically based on metrics that you define.\r\n\r\nIn this talk, you’ll learn how to design, package & deploy reliable .NET applications to Kubernetes & decouple several components using a message broker. You will also learn how to set autoscaling rules to cope with an increasing influx of messages in the queue.", - "startsAt": "2019-01-31T15:00:00", - "endsAt": "2019-01-31T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "964af3ac-fd5e-46f6-b582-6c0d2da30db4", - "name": "Lewis Denham-Parry" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10570, - "name": "Concurrency" - }, - { - "id": 10571, - "name": "Continuous Delivery" - }, - { - "id": 10576, - "name": "Docker" - }, - { - "id": 10575, - "name": "DevOps" - }, - { - "id": 10572, - "name": "Cross-Platform" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10586, - "name": "Microsoft" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "56795", - "title": "Zero to Mobile Hero - Intro to Xamarin and Visual Studio Team Services", - "description": "You can be faced with a nightmare of Xcode, Android Studio, Swift, Objective C, Swift and other options. This means not only learning multiple languages and frameworks but also having to support two different codebases for the same application. But Xamarin Native and Xamarin.Forms offer a powerful, cross-platform development solution for .NET developers looking to target smartphones, tablets, TV’s, computers and IoT devices.\r\n\r\nIn this talk, Luce shares what Xamarin is including Native and Xamarin.Forms for both C# and F#, how to get started creating a simple HelloWorld app from scratch and a more complex example (will involve at least one Azure service including Cognitive Services for facial recognition). Also how to use Visual Studio Team Services for Continuous Integration and some awesome examples of apps written using Xamarin including ones used to save lives!\r\n\r\nLuce will take examples from xamarin.com/customers as well as show this demo about how Xamarin was used alongside other technologies to aid with Skin Cancer prediction.\r\n\r\nThis talk will include slides, demos, code samples, live coding and the audience will walk away feeling like they too can create a mobile app in just a few minutes and carry their work around with them in their pocket or backpack!", - "startsAt": "2019-01-31T15:00:00", - "endsAt": "2019-01-31T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "79741134-06bb-4596-bc44-40edd9e03f6b", - "name": "Luce Carter" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10565, - "name": "AI" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10572, - "name": "Cross-Platform" - }, - { - "id": 10586, - "name": "Microsoft" - }, - { - "id": 10587, - "name": "Mobile" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "68161", - "title": "ML.NET for developers without any AI experience", - "description": "AI talk is everywhere but where do you start as a .NET developer? During this session, we will explore how you can use AI in the applications your creating today. How to start building & training your ML models with your .NET skills through ML.NET. \r\n\r\nWant to detect laughter in a phone conversation? Detect the mood of Jira tickets or predict if/where your code has bugs. This session will get you started with AI and ML.NET.", - "startsAt": "2019-01-31T15:00:00", - "endsAt": "2019-01-31T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "8061571c-bb36-42a9-b714-4d90c98529fd", - "name": "Lee Mallon" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10565, - "name": "AI" - }, - { - "id": 10584, - "name": "Machine Learning" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "66437", - "title": "(WPF + WinForms) * .NET Core = Modern Desktop", - "description": "Learn how .NET Core 3 brings WPF and Windows Forms into the future with a modern runtime. See what’s new for WPF and Windows Forms, learn how to easily retarget your .NET Framework application over to .NET Core, and how to get these modern desktop apps to your users.\r\n", - "startsAt": "2019-01-31T15:00:00", - "endsAt": "2019-01-31T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "8b1783d3-15ce-41dc-b989-386759803d97", - "name": "Oren Novotny" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10594, - "name": "UI" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - }, - { - "id": 2780, - "name": "Room 6", - "session": { - "id": "66322", - "title": "Accessible App Design", - "description": "Want to start building apps for people with disabilities but doesn’t no where to start? In this session we cover everything to start right now! There are many people with learning disabilities around the world. There are also people with autism, psychological disabilities… Many of them are also entering the era of mobile and internet. We talk about design guidelines that can reach them. \r\n\r\nWe talk about apps, but do not focus on a specific technology in this talk. Which apps are needed? Which steps of design helps them to catch the frontiers of mobile and internet right now? Microsoft and other big tech companies are working more and more about inclusive design and digital inclusion for people with disabilities. ", - "startsAt": "2019-01-31T15:00:00", - "endsAt": "2019-01-31T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "67a075a3-a111-4116-bd44-a25a01485959", - "name": "Dennie Declercq" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10591, - "name": "Soft Skills" - }, - { - "id": 10594, - "name": "UI" - }, - { - "id": 10595, - "name": "UX" - } - ], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - "index": 5 - } - ] - }, - { - "slotStart": "16:20:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "66795", - "title": "DiagnosticSourcery 101", - "description": ".NET has a new mechanism for generating and storing diagnostic data: DiagnosticSource. This is the cross-platform alternative to ETW. Much of ASP.NET Core and EF Core produce useful metric data using DiagnosticSource, and you can produce your own and stream some or all of the data to the metrics storage of your choice.\r\n\r\nIn this talk I'll run through how DiagnosticSource works, show you how to use it to output your own metrics in any .NET application, and how to pipe those metrics to a Time-Series database and turn them into a lovely Grafana dashboard.\r\n\r\nYou can use DiagnosticSource in anything from an ASP.NET Core cloud-native microservice to a WPF desktop application, and it's a Microsoft package with no 3rd-party dependencies, so this talk should be interesting and useful for any .NET developer.", - "startsAt": "2019-01-31T16:20:00", - "endsAt": "2019-01-31T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "70590ad0-b1b3-40b8-b05d-b58722ef9d9d", - "name": "Mark Rendle" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10572, - "name": "Cross-Platform" - }, - { - "id": 10575, - "name": "DevOps" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "68608", - "title": "A practical guide to deep learning", - "description": "Machine Learning is one of the fastest growing areas of computer science, and Deep Learning (neural networks) is growing even faster, with lots of data and computing power at our fingertips. \r\nThis talk is a practical (very little math) guide to computer vision and deep learning.\r\n\r\nWe will look at a deep learning project from start to finish, look at how to program and train a neural network and gradually refine it using some tips and tricks that you can steal for your future deep learning projects.", - "startsAt": "2019-01-31T16:20:00", - "endsAt": "2019-01-31T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "cb70e0f2-0f0b-4b8f-b3f4-a9326309a1b2", - "name": "Tess Ferrandez-Norlander" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10584, - "name": "Machine Learning" - }, - { - "id": 10565, - "name": "AI" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "57812", - "title": "Build cross-platform mobile apps using Fabulous", - "description": "In recent years there has been a shift in the way websites and mobile apps are being built - moving to architectures with immutable models and virtual UIs - based on the MVU (model-view-update) pattern. This has lead to great new frameworks like ELM and React for web, and ReactNative for mobile.\r\n\r\nNow there is a new MVU framework for building mobile apps - Fabulous. It's a community-driven open source framework, combining the simplicity of an MVU framework, with 100% native API access for both iOS and Android, all built with using an established, world class, battle-hardened functional programming language.\r\n\r\nThis session will start with an overview of MVU, discussing how it works and why it is such a great architecture. It will then move on to building your first Fabulous app that runs on iOS and Android. Next up more features will be added to the app whilst the app is running on a device, showing the hot reload capabilities of Fabulous for both UI and app logic. Finally it will look at the underlying architecture, see how to use all of the iOS and Android APIs, see how to easily use native components such as cocoa pods or jars, and look at the massive range of libraries that this framework as available to it to do all manner of UI and application logic things. We'll even see how to use it on macOS and Windows, including being able to build iOS apps on Windows (with the help of a networked Mac, Apple licensing rules and whatnot).\r\n\r\nWhen looking at naming for this framework, someone suggested Fabulous. By the end of this session you will see why that name stuck.", - "startsAt": "2019-01-31T16:20:00", - "endsAt": "2019-01-31T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "75465aaf-45f4-4d3c-b78a-2e3d5206b57f", - "name": "Jim Bennett" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10587, - "name": "Mobile" - }, - { - "id": 10586, - "name": "Microsoft" - }, - { - "id": 10580, - "name": "Functional Programming" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "68212", - "title": "Reverse Engineering a Bluetooth Lightbulb", - "description": "I recently made build lights for the company I work for and my home office. They integrate with TeamCity and indicate when a build is running and success/failure of all the tests. In this session, we will reverse engineer a bluetooth light bulb’s protocol, learn how to have an Raspberry Pi communicate with the bulb, and by the end you too will know how to make your own build lights! Please note that this talk will be highly technical. We will be discussing low level details of bluetooth communication, protocol analysis with Wireshark, sniffing bluetooth packets, etc.", - "startsAt": "2019-01-31T16:20:00", - "endsAt": "2019-01-31T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "86d72d85-ef02-405e-b0de-1603dae6b8fb", - "name": "Jesse Phelps" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10606, - "name": "Embedded" - }, - { - "id": 10581, - "name": "IoT" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "66866", - "title": "An Introduction to WebAssembly", - "description": "Want to write a web application? Better get familiar with JavaScript JavaScript has long been the king of front-end. While there have been various attempts to dethrone it, they have typically involved treating JavaScript as an assembly-language analog that you transpile your code to. This has lead to complex build pipelines that result in JavaScript which the browser has to parse and *you* still have to debug. But what if there were an actual byte-code language you could compile your non-JavaScript code to instead? That is what WebAssembly is.\r\n\r\nI'm going to explain how WebAssembly works and how to use it in this talk. I'll cover what it is, how it fits into your application, and how to build and use your own WebAssembly modules. And, I'll demo how to build and use those modules with both Rust and the WebAssembly Text Format. That's right, I'll be live coding in an assembly language. I'll also go over some online resources for other languages and tools that make use of WebAssembly.\r\n\r\nWhen we're done, you'll have the footing you need to start building applications featuring WebAssembly. So grab a non-JavaScript language, a modern browser, and let's and get started!", - "startsAt": "2019-01-31T16:20:00", - "endsAt": "2019-01-31T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "520bf4ca-b2d3-47c3-8475-c25bb2b257f7", - "name": "Guy Royse" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10596, - "name": "Web" - }, - { - "id": 10582, - "name": "JavaScript" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - }, - { - "id": 2780, - "name": "Room 6", - "session": { - "id": "58551", - "title": "System Stable : Robust connected applications with Polly, the .NET Resilience Framework", - "description": "Short Description\r\n\r\nJoin me for this session and I will show you how with just a few lines of code, you can make your applications much more resilient and reliable. With Polly, the .NET resilience framework, your application can easily tolerate transient faults and longer outages in remote systems or infrastructure. \r\n\r\nAt the end of this hour you will know how to use all the features of Polly to make your application a rock solid piece of work. We’ll cover the reactive and proactive resilience strategies, starting with simple but very powerful retries and finishing with bulkhead isolation.\r\nOh, and did I mention Polly is a .NET Standard library so it will work on any application you can think of! Join me for an hour, and your applications will never be the same.\r\n\r\n----------------------------------------------------------------------------------------------------------------------------------\r\n\r\nFull Description\r\n\r\nJoin me for this session and I will show you how with just a few lines of code, you can make your applications much more resilient and reliable. Let me tell you more… \r\n\r\nAlmost all applications now depend on connectivity, but what do you do when the infrastructure is unreliable or the remote system is down or returns an error? Does your application grind to a halt or just drop that single request? What if you could recover from these kinds of error, maybe even so quickly it won’t be noticed? \r\n \r\nWith Polly your application can easily tolerate transient faults and longer outages in remote systems or infrastructure. \r\n\r\nAt the end of this hour you will know how to use all the features of Polly to make your application a rock solid piece of work. \r\n\r\nWe’ll start with the simple but very powerful Retry Policy which lets you retry a failed request. If simple retries are not good enough for you, there is a Wait and Retry policy which introduces a delay between retries, giving the remote service time to recover before being hit again. Then I show you how to use the circuit breaker for when things have really gone wrong and a remote system is struggling under too much load or has failed. If all these attempts are unsuccessful and you are still not getting through to the remote system, you can return a default response or execute arbitrary code to call for human help (or restart the cloud) with the fallback policy. \r\n\r\nThat takes care of what you can do when things go wrong, but Polly also lets you take proactive steps to keep your application and the services it depends on healthy. \r\n\r\nTo get you started with proactive strategies, you will learn how caching can be used to store and return responses from an in-memory or distributed cache without having to hit the remote server every time. Or you can use bulkhead isolation to marshal resources within your application so that no one struggling part can take down the whole. Finally, I’ll show you how to fail quickly when your application is in danger of being overloaded or the remote systems is not responding in a timely fashion, this is done with the bulkhead isolation and the timeout polices. \r\n\r\nOh, and did I mention Polly is a .NET Standard library so it will work on any application you can think of! Join me for an hour, and your applications will never be the same. ", - "startsAt": "2019-01-31T16:20:00", - "endsAt": "2019-01-31T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "de972e57-7765-4c38-9dcd-5981587c1433", - "name": "Bryan Hogan" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10596, - "name": "Web" - }, - { - "id": 10587, - "name": "Mobile" - }, - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - "index": 5 - } - ] - }, - { - "slotStart": "17:40:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "63671", - "title": "Let's Talk HTTP in .NET Core", - "description": "In the world of microservices (yes, there's that buzzword again!) and distributed systems, we often find ourselves communicating over HTTP. What seems like a simple requirement can quickly become complicated! Networks aren't reliable and services fail. Dealing with those inevitable facts and avoiding a cascading failure can be quite a challenge. In this talk, Steve will explore how we can build .NET Core applications that make HTTP requests and rely on downstream services, whilst remaining resilient and fault tolerant.\r\n\r\nThis session will focus on some of the improvements which have been released in .NET Core and ASP.NET Core 2.1, such as IHttpClientFactory and the new, more performant SocketHttpHandler. Steve will identify some HTTP anti-patterns and common mistakes and demonstrate how we can refactor existing code to use the new HttpClientFactory features.\r\n\r\nNext, Steve will demonstrate other HTTP tips and tricking, including Polly; a fantastic resilience and transient fault handling library which can be used to make your applications less prone to failure. When integrated with the Microsoft IHttpClientFactory; wrapping your HTTP calls in retries, timeouts and circuit-breakers has never been easier!\r\n\r\nIf you're building services which make HTTP calls, then this talk is for you!", - "startsAt": "2019-01-31T17:40:00", - "endsAt": "2019-01-31T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "26b7e78d-aa31-4d7f-b2ea-90fef14d1087", - "name": "Steve Gordon" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10586, - "name": "Microsoft" - }, - { - "id": 10585, - "name": "Microservices" - }, - { - "id": 10596, - "name": "Web" - }, - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10569, - "name": "Cloud" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "69332", - "title": ".NET Rocks Live on Software Feature Selection with Christine Yen", - "description": "Join Carl and Richard from .NET Rocks as they chat with Christine Yen from Honeycomb about how you select features to build in your applications.\r\n\r\nAfter the first version of software is out the door, what do you choose nest? Christine has a background in instrumenting applications to understand what people use – is that the best way to pick features? What about the vision of your own designers? What about asking the users? Bring your questions and come to this live recording of .NET Rocks!", - "startsAt": "2019-01-31T17:40:00", - "endsAt": "2019-01-31T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "d4d7850a-526e-4959-aab1-36460081fcf5", - "name": "Richard Campbell" - }, - { - "id": "bf171bb6-4c78-4fe9-ac77-2338d5ec7975", - "name": "Carl Franklin" - }, - { - "id": "e5d55e8e-93d1-4a53-bbec-10e4beaf5fd8", - "name": "Christine Yen" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "69228", - "title": "Dungeons, Dragons and Functions", - "description": "Dungeons & Dragons, or D&D, is the grand-daddy of all role playing games. While playing D&D is great fun, the rules are a bit daunting for the beginner. The basic rulebook, the PHB, clocks in at a solid 300 pages, and can be extended with multiple additional rule sets. This should come as no surprise to software engineers: this is, after all, documentation for a system that models a complex domain, and has been in use for over 40 years now, going through numerous redesigns over time.\r\n\r\nAs such, D&D rules make for a great exercise in domain modelling. In this talk, I will take you along my journey attempting to tame that monster. We will use no magic, but a weapon imbued with great power, F#; practical tips and tricks for the Adventurer on the functional road will be shared. So... roll 20 for initiative, and join us for an epic adventure in domain modeling with F#!", - "startsAt": "2019-01-31T17:40:00", - "endsAt": "2019-01-31T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "14879952-64d2-42b5-8d6f-d1e1a68b39c9", - "name": "Mathias Brandewinder" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "64248", - "title": "Data on the Inside, Data on the Outside", - "description": "When we move from a monolith to microservices we abandon integrating via a shared database, as each service must own its own data to allow them it to be autonomous. But now we have a new problem, our data is distributed. What happens if I need one service needs to talk to another about a shared concept such as a product, a hotel room, or an order? Does every service need to have a list of all our users? Who knows what users have permissions to the entities within the micro service? What happens if my REST endpoint needs to include data from a graph that includes other services to make it responsive? And I am not breaking the boundary of my service when all of this data leaves my service boundary in response to a request?\r\n\r\nNaive solutions result in chatty calls as each service engages with multiple other services to fulfil a request, or in large message payloads as services add all the data required to process a message to each message. Neither scale well.\r\n\r\nIn 2005, Pat Helland wrote a paper ‘Data on the Inside vs. Data on the Outside’ which answers the question by distinguishing between data a service owns and reference data that it can use. In this presentation we will explain reference data, how it is classified, and how it should be implemented. We will include a discussion of using reference data from ATOM feeds, discrete messaging and event streams. We’ll provide examples in C#, Python and Go as well as using RMQ and Kafka.", - "startsAt": "2019-01-31T17:40:00", - "endsAt": "2019-01-31T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "84ffe422-5bc6-4075-9903-d9ad3526cf86", - "name": "Ian Cooper" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10585, - "name": "Microservices" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "68009", - "title": "Scaling Frontend Development", - "description": "Abstract\r\nLarge frontend projects suffer from all of the same problems of other software projects with the same characteristics:\r\nCommunication and coordination overhead\r\nSpeed of delivery\r\nLarge quantum size\r\nLow productivity \r\n\r\nThese factors are growing concerns for companies as productivity and speed of delivery is nowadays crucial for success in this competitive landscape\r\n\r\nA common way of solving many of these issues in software projects is the microservices approach.\r\n\r\nToday a similar approach of applying this same pattern to the frontend is being popularized and is commonly nominated as micro-frontends.\r\n\r\nHowever this approach has many fallacies because of frontend specific characteristics:\r\n- The performance cost of loading many independent applications\r\n- The need for a common visual language and experience\r\n- The browser is a monolithic runtime \r\n\r\nDescription\r\nThis talk will be about the problems of large frontend projects and how to scale them in a way that balances team autonomy with performance and productivity\r\n\r\nI’ll talk about how at farfetch we’re progressively refactoring our frontend to multiple independent applications, and the strategies we’re using to make sure that despite having many applications made by different independent and geographically distributed teams we still can deliver a performant and consistent product to the end user.\r\n", - "startsAt": "2019-01-31T17:40:00", - "endsAt": "2019-01-31T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "f79c1f81-b367-4ce0-9cc0-4263f30a9f7f", - "name": "luis vieira" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10594, - "name": "UI" - }, - { - "id": 10585, - "name": "Microservices" - }, - { - "id": 10582, - "name": "JavaScript" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - }, - { - "id": 2780, - "name": "Room 6", - "session": { - "id": "68611", - "title": "Clean Architecture with ASP.NET Core 2.2", - "description": "The explosive growth of web frameworks and the demands of users have changed the approach to building web applications. Many challenges exist, and getting started can be a daunting prospect. Let's change that now.\r\n\r\nThis talk provides practical guidance and recommendations. We will cover architecture, technologies, tools, and frameworks. We will examine strategies for organizing your projects, folders and files. We will design a system that is simple to build and maintain - all the way from development to production. You leave this talk inspired and prepared to take your enterprise application development to the next level.", - "startsAt": "2019-01-31T17:40:00", - "endsAt": "2019-01-31T18:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "3a5abb12-c9e2-4641-8ace-a4c067ad8e8d", - "name": "Jason Taylor" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - "index": 5 - } - ] - } - ] - }, - { - "date": "2019-02-01T00:00:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "sessions": [ - { - "id": "69232", - "title": "Keynote: The Microsoft Open Source Cinematic Universe - Phase 2", - "description": "Phase 1 is nearly complete with open source .NET Core. What does Microsoft's Open Source plan look like for the next 10 years?\r\n\r\nJoin Scott Hanselman as he compares the MCU (Marvel Cinematic Universe) to the MSFTOSSCU and talks about what a next phase MIGHT look like.", - "startsAt": "2019-02-01T09:00:00", - "endsAt": "2019-02-01T10:00:00", - "isServiceSession": false, - "isPlenumSession": true, - "speakers": [ - { - "id": "142dd289-d6c7-4385-9f36-05b816ceea2c", - "name": "Scott Hanselman" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - { - "id": "69004", - "title": "Async injection", - "description": "This talk attempts to answer a pair of frequently asked questions, the first one of which is: how do I combine dependency injection with async and await in C# without leaky abstractions?\r\n\r\nIt turns out that the answer to that question can be found by answering another frequently asked question: how do I get the value out of my monad?\r\n\r\nDuring the talk, you’ll get a quick and easy-to-understand explanation of monads.\r\n\r\nAll code examples will be in C#.", - "startsAt": "2019-02-01T10:20:00", - "endsAt": "2019-02-01T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "84f7d361-4388-44ad-bc48-31695762bc6d", - "name": "Mark Seemann" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10580, - "name": "Functional Programming" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - { - "id": "68521", - "title": "From 'dotnet run' to 'Hello World!'", - "description": "Have you ever stopped to think about all the things that happen when you execute a simple .NET program?\r\n\r\nThis talk will delve into the internals of the recently open-sourced .NET Core runtime, looking at what happens, when it happens and why. \r\n\r\nMaking use of freely available tools such as 'PerfView', we'll examine the Execution Engine, Type Loader, Just-in-Time (JIT) Compiler and the CLR Hosting API to see how all these components play a part in making 'Hello World' possible.", - "startsAt": "2019-02-01T11:40:00", - "endsAt": "2019-02-01T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "fb89e2c6-39b1-4ef8-8932-609504cf4901", - "name": "Matt Warren" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - { - "id": "69233", - "title": "Solving Diabetes with an Open Source Artificial Pancreas", - "description": "Scott has been a Type 1 diabetic for over 20 years. When he first became diabetic he did what every engineer would do...he wrote an app to solve his problem. Fast forward to 2018 and Scott lives 24 hours a day connected to an open source artificial pancreas. After years of waiting, the diabetes community online creating solutions.\r\n\r\nScott will go through the history of diabetes online, the components (both hardware and software) needed for an artificial pancreas, and discuss the architectural design of two popular systems (LoopKit and OpenAPS). Plus, you'll see Scott *not die* live on stage as he's been \"looping\" for over a year!", - "startsAt": "2019-02-01T13:40:00", - "endsAt": "2019-02-01T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "142dd289-d6c7-4385-9f36-05b816ceea2c", - "name": "Scott Hanselman" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - { - "id": "69010", - "title": "Crash, Burn, Report", - "description": "With the launch of the Reporting API any browser that visits your site can automatically detect and alert you to a whole heap of problems with your application. DNS not resolving? Serving an invalid certificate? Got a redirect loop, using a soon to be deprecated API or any one of countless other problems, they can all be detected and reported with no user action, no agents, no code to deploy. You have one of the most extensive and powerful monitoring platforms in existence at your disposal, millions of browsers. Let's look at how to use them.\r\n\r\nIn this talk we'll look at how to configure the browser to send you reports when things go wrong. These are brand new capabilities the likes of which we've haven't seen before and they're already supported in the world's most popular browser, Google Chrome. We'll look at how to receive reports and how to make use of them after having the browser do the hard work.", - "startsAt": "2019-02-01T15:00:00", - "endsAt": "2019-02-01T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "e9c64362-6194-409c-85da-45c8fd40abd6", - "name": "Scott Helme" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - { - "id": "66952", - "title": "Futurology for Dummies - the Next 30 Years in Tech", - "description": "2019 is the 30th anniversary of my first job in tech. On my first day I was given a Wyse 60 terminal attached via RS232 cables to a Tandon 286, and told to learn C from a dead tree so I could write text applications for an 80x24 character screen. Fast-forward to now: my phone is about a million times more powerful than that Tandon; screens are 3840x2160 pixels; every computer in the world is attached to every other thing with no cables; and we code using, well, still basically C.\r\n\r\nHaving lived through all those changes in realtime, and as an incurable neophile, I think I can make an educated guess as to what the next 30 years are going to be like, and what we're all going to be doing by 2049. If anything, I'm going to underestimate it, but hopefully you'll be inspired, invigorated and maybe even informed about the future of your career in tech.", - "startsAt": "2019-02-01T16:20:00", - "endsAt": "2019-02-01T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "70590ad0-b1b3-40b8-b05d-b58722ef9d9d", - "name": "Mark Rendle" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10579, - "name": "Fun" - }, - { - "id": 10584, - "name": "Machine Learning" - }, - { - "id": 10581, - "name": "IoT" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10565, - "name": "AI" - }, - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10594, - "name": "UI" - }, - { - "id": 10595, - "name": "UX" - }, - { - "id": 10596, - "name": "Web" - }, - { - "id": 10574, - "name": "Design" - }, - { - "id": 10587, - "name": "Mobile" - }, - { - "id": 10604, - "name": "3D Modeling" - }, - { - "id": 10603, - "name": "VR" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - } - ], - "hasOnlyPlenumSessions": false - }, - { - "id": 2776, - "name": "Room 2", - "sessions": [ - { - "id": "69239", - "title": "Ctrl-Alt-Del: Learning to Love Legacy Code", - "description": "The world runs on legacy code. For every greenfield progressive web app with 100% test coverage, there are literally hundreds of archaic line-of-business applications running in production - systems with no tests, no documentation, built using out-of-date tools, languages and platforms. It’s the code developers love to hate: it’s not exciting, it’s not shiny, and it won’t look good on your CV - but the world runs on legacy code, and, as developers, if we’re going to work on anything that actually matters, we’re going to end up dealing with legacy. To work effectively with this kind of system, we need to answer some fundamental questions: why was it built this way in the first place? What's happened over the years it's been running in production? And, most importantly, how can we develop our understanding of legacy codebases to the point where we're confident that we can add features, fix bugs and improve performance without making things worse?\r\n\r\nDylan worked on the web application stack at Spotlight (www.spotlight.com) from 2000 until 2018 - first as a supplier, then as webmaster, then as systems architect. Working on the same codebase for nearly two decades has given him an unusual perspective on how applications go from being cutting-edge to being 'legacy'. In this talk, he'll share tips, patterns and techniques that he's learned from helping new developers work with a large and unfamiliar codebase. We'll talk about virtualisation, refactoring tools, and how to bring legacy code under control using continuous integration and managed deployments. We'll explore creative ways to use common technologies like DNS to create more productive development environments. We'll talk about how to bridge the gap between automated testing and systems monitoring, how to improve visibility and transparency of your production systems - and why good old Ctrl-Alt-Del might be the secret to unlocking the potential of your legacy codebase.", - "startsAt": "2019-02-01T10:20:00", - "endsAt": "2019-02-01T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "1d7dcbfc-1de6-4228-8bd6-04f4ba1c4267", - "name": "Dylan Beattie" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10571, - "name": "Continuous Delivery" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - { - "id": "67993", - "title": "The Functional Programmer's Toolkit", - "description": "The functional programming community has a number of patterns with strange names such as monads, monoids, functors, and catamorphisms.\r\n \r\nIn this beginner-friendly talk, we'll demystify these techniques and see how they all fit together into a small but versatile \"tool kit\". \r\n\r\nWe'll then see how the tools in this tool kit can be applied to a wide variety of programming problems, such as handling missing data, working with lists, and implementing the functional equivalent of dependency injection. ", - "startsAt": "2019-02-01T11:40:00", - "endsAt": "2019-02-01T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "89335661-3092-4afa-b53f-3d203cafa2a1", - "name": "Scott Wlaschin" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10580, - "name": "Functional Programming" - }, - { - "id": 10574, - "name": "Design" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - { - "id": "69230", - "title": "Society (n+1).0: smashing the patriarchy and other ways of changing the world", - "description": "Software engineers are fond of casually mentioning how their work changes the world. But have you thought about what you want the world to look like after you've changed it? Do you want to smash the patriarchy [1]? Save the planet? Make Furbies cool again? Is technology really the only tool at your disposal?\r\nThis session is a call to both discovery and action. You already care about Furbies, but what do you not know you don't know about them? Have you looked at Furbies from someone else's perspective? Once your eyes are wide open, what's the next step? How would a Furby smash the patriarchy? Let's learn to be better together, and make a difference in the world today.\r\n\r\nFootnote 1: Our system of society norms which puts everyone in boxes that are hard to escape [2]. The boxes surrounding straight white men can be extremely comfortable ones, but more limiting than you might realize.\r\n\r\nFootnote 2: Furbies are pretty easy to get out of boxes; humans, not so much.", - "startsAt": "2019-02-01T13:40:00", - "endsAt": "2019-02-01T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "1176547b-89c5-40ee-b7d7-bf0eb5ca0aef", - "name": "Jon Skeet" - }, - { - "id": "e5b9e6c6-1477-4b19-8347-06683a7417de", - "name": "Jennifer Wadella" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - { - "id": "69307", - "title": "Tabs, spaces and salaries: a data science detective story", - "description": "In data science, sometimes you stumble across an intriguing property in the data. I will tell you a story of a mysterious correlation - from the StackOverflow developer survey it seems that developers who use spaces have higher salaries than those who use tabs. Correlation doesn't mean causation: using spaces won't suddenly increase your salary. But what does it all mean? Follow me into a detective investigation that will show you how to approach complex data science problems. I will show you some of the perils of correlation, model fitting and biases - how they can be dangerous and how to avoid these traps. And you'll also find how profitable your indentation style really is.", - "startsAt": "2019-02-01T15:00:00", - "endsAt": "2019-02-01T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "14fdabdd-3814-4f84-868e-a8178a25c5f8", - "name": "Evelina Gabasova" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10584, - "name": "Machine Learning" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - { - "id": "67994", - "title": "Four Languages from Forty Years Ago", - "description": "The 1970's were a golden age for new programming languages, but do they have any relevance to programming today? Can we still learn from them?\r\n\r\nIn this talk, we'll look at four languages designed over forty years ago -- SQL, Prolog, ML, and Smalltalk -- and discuss their philosophy and approach to programming, which is very different from most popular languages today.\r\n\r\nWe'll come away with some practical principles that are still very applicable to modern development. And you might discover your new favorite programming paradigm!\r\n", - "startsAt": "2019-02-01T16:20:00", - "endsAt": "2019-02-01T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "89335661-3092-4afa-b53f-3d203cafa2a1", - "name": "Scott Wlaschin" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10583, - "name": "Languages" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - } - ], - "hasOnlyPlenumSessions": false - }, - { - "id": 2777, - "name": "Room 3", - "sessions": [ - { - "id": "69231", - "title": "C# 8", - "description": "It's nearly here! The Visual Studio 2019 Preview is already available, so if you haven't looked into what's coming in C# 8, now is the perfect time to do so.\r\nThe most important feature of C# 8 is undoubtedly nullable reference types, but there's plenty more to look forward to as well.\r\n\r\nWhile I'll make this talk as easy to understand as I can, there's a huge amount to cover. Expect a fast pace, with lots of code.", - "startsAt": "2019-02-01T10:20:00", - "endsAt": "2019-02-01T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "1176547b-89c5-40ee-b7d7-bf0eb5ca0aef", - "name": "Jon Skeet" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - { - "id": "67078", - "title": "APIs Exposed!", - "description": "More and more developers are building APIs, whether that be for consumption by client-side applications, exposing endpoints directly to customers so they can use an alternative front-end or wrapping up services in containers.\r\n\r\nNow that we have all these exposed endpoints, what are we doing to secure them? Previously, our monolith was self-contained with limited points of access making authentication and authorisation more straightforward - that’s no longer the case.\r\n\r\nWe’ll cover the potential risks we may face such as cross-site scripting and BruteForce attacks as well as a look at the possible options for securing API endpoints including OAuth, Access Tokens, JSON web tokens, IP whitelisting, rate limiting to name but a few.\r\n\r\n", - "startsAt": "2019-02-01T11:40:00", - "endsAt": "2019-02-01T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "ac105033-2b20-4b45-8d47-5647d8accf84", - "name": "Layla Porter" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10589, - "name": "Security" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - { - "id": "69366", - "title": "Architectural patterns for the cloud", - "description": "As more and more people are starting to deploy their application to the cloud, new architectural patterns have emerged, and some old ones have resurfaced or become more prominent.\r\n\r\nIn this session, Mahesh Krishnan, will run through a large catalogue of cloud patterns that will cover categories such as availability, resiliency, data management, performance, scalability, design and implementation. You will learn about what problem each and every pattern addresses, how to implement them and what considerations you need to take into account while implementing them.", - "startsAt": "2019-02-01T13:40:00", - "endsAt": "2019-02-01T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "648e2e9d-28cc-47ef-828c-75a7d599d48d", - "name": "Mahesh Krishnan" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10569, - "name": "Cloud" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - { - "id": "66814", - "title": "Unity 101 for C# Developers", - "description": "If you are interested in mixed or virtual reality development then Unity is a tool you’ll want to learn. Go to Microsoft’s [Hololens development page](https://developer.microsoft.com/en-us/windows/mixed-reality/install_the_tools) and you’ll find the installation of Unity recommended in the first paragraph.\r\n\r\nOne surprise to developers getting started with Unity is the level of integration with Visual Studio and how easy it is to add C# to manipulate your 3D world.\r\n\r\nIn this demo led session we’ll go back to basics with Unity, easily creating a simple 3D experience with realistic physics. We’ll look at the fantastic Visual Studio integration and how easily the two editors work together.\r\n\r\nFinally we’ll look at the ways in which we can give our 3D experience some polish by adding textures, animations and some explosions!\r\n\r\nKeeping the best news for last: Unity is royalty free until you’re earning $100,000 – so if you like the idea of becoming wildly rich from making immersive 3D experiences this is the talk for you.", - "startsAt": "2019-02-01T15:00:00", - "endsAt": "2019-02-01T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "ef4df68d-9bdc-471d-aff0-075c77c4a424", - "name": "Andy Clarke" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10603, - "name": "VR" - }, - { - "id": 10604, - "name": "3D Modeling" - }, - { - "id": 10602, - "name": "Gaming" - }, - { - "id": 10579, - "name": "Fun" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - { - "id": "67138", - "title": "Deep Learning in the world of little ponies", - "description": "In this talk, we will discuss computer vision, one of the most common real-world applications of machine learning. We will deep dive into various state-of-the-art concepts around building custom image classifiers - application of deep neural networks, specifically convolutional neural networks and transfer learning. We will demonstrate how those approaches could be used to create your own image classifier to recognise the characters of \"My Little Pony\" TV Series [or Pokemon, or Superheroes, or your custom images].", - "startsAt": "2019-02-01T16:20:00", - "endsAt": "2019-02-01T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "b4c506ac-9757-4ac6-8b7c-3d6696b113e4", - "name": "Galiya Warrier" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10584, - "name": "Machine Learning" - }, - { - "id": 10579, - "name": "Fun" - }, - { - "id": 10565, - "name": "AI" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - } - ], - "hasOnlyPlenumSessions": false - }, - { - "id": 2778, - "name": "Room 4", - "sessions": [ - { - "id": "58161", - "title": "Making Accessibility Testing Suck Less: An Intro to Pa11y.", - "description": "Often the hardest part of any problem is simply how to get started. On the ever-evolving web accessibility is a matter of ongoing importance: the brilliance of your code or sleekness of your UI is inconsequential if your app or website is unusable to some of your users. With a million other issues already on your plate how do you find a way to get started on accessibility testing? Pa11y to the rescue! Pa11y is a lightweight command-line accessibility testing tool with enough flexibility to integrate results into your current testing process. This talk will explain what pa11y does and does not cover, review examples of both command line and scripted usage, dive into the pa11y web service and show how to modify output to work in your current testing setup. Bonus content: how to convince the rest of your team and business why accessibility is worth prioritizing and how getting started with low-hanging fruit can vastly improve your product.", - "startsAt": "2019-02-01T10:20:00", - "endsAt": "2019-02-01T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "e5b9e6c6-1477-4b19-8347-06683a7417de", - "name": "Jennifer Wadella" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10592, - "name": "Testing" - }, - { - "id": 10582, - "name": "JavaScript" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - { - "id": "62532", - "title": "A Developer's Guide to Advertising", - "description": "Love it or hate it, advertising is currently a main source of revenue for many websites. But digital advertising is a complicated space that not many developers understand. If you'd like to understand the ad code on your site better, if your ad ops team has been talking about header bidding, or if you're just curious about how ads work, this is the talk for you. I'll give an overview of how programmatic advertising works, what header bidding is, explain some common tools and libraries, and address some common misconceptions and complaints about ads.", - "startsAt": "2019-02-01T11:40:00", - "endsAt": "2019-02-01T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "b8f3c690-5950-4279-9ee7-9291d1439dd2", - "name": "Krista LaFentres" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10596, - "name": "Web" - }, - { - "id": 10582, - "name": "JavaScript" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - { - "id": "68894", - "title": "\"Hey Mycroft\": Getting Started with the OSS Home Assistant", - "description": "Home virtual assistants, like Alexa, Google Now, Siri, and Cortana, are gaining a lot of popularity. They’re now incorporated into our phones, our laptops, and even available as separate devices in our homes. Some people haven’t adopted them out of privacy concerns. A new system called Mycroft has come onto the scene, and it’s built on open source hardware and software. You can install it on a Raspberry Pi, an old Linux box, or buy their own Mycroft device.\r\n\r\nIn this session, we’ll go over the basics of what Mycroft is, and how you can quickly install it yourself. From there, we’ll talk about some of the underlying software and see a short demo. Finally, we’ll see how to build a new skill into it and contribute it back to the community. You’ll leave with your own virtual assistant and the knowledge on how make it do what you want but keep your privacy in check.", - "startsAt": "2019-02-01T13:40:00", - "endsAt": "2019-02-01T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "97e1c796-8bf6-468b-b246-0efc6979b9e7", - "name": "Sarah Withee" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10581, - "name": "IoT" - }, - { - "id": 10565, - "name": "AI" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - { - "id": "68882", - "title": "Megahertz, Gigahertz, Registers and Instructions: How does a CPU actually work?", - "description": "For decades, we’ve been creating ever higher abstractions between ourselves and the computing hardware we’re programming, but in the end whether you’re writing JavaScript, Haskell, or Python it all comes down to 1’s and 0’s running through hardware patterns that were well understood twenty years ago.\r\n\r\nWe’ll walk through the fundamentals of how CPU’s “think” in an accessible way (no engineering degree required!) so you can appreciate the marvel that is the modern CPU be it in a server data center or your fridge at home. You’ll learn how a CPU turns the code we feed it into actions, what’s the big difference between an ARM and an Intel processor, how CPU’s constantly optimize work for us, and where is it all going for the next few years.\r\n\r\nFinally, we'll show how Meltdown and Spectre take advantage of CPU internals to expose data and why this class of security problems are going to be a challenge to the next generation of CPU's.\r\n", - "startsAt": "2019-02-01T15:00:00", - "endsAt": "2019-02-01T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "fe60954c-9d1a-4167-9faa-77027c8d446b", - "name": "Kendall Miller" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - { - "id": "67872", - "title": "Distributed Tracing and Monitoring with OpenCensus", - "description": "OpenCensus is an emerging standard for tracing and metrics of cloud services. You can use it to gain observability into applications that span multiple clouds and technological stacks.\r\nIn this talk, we will use open source and vendor agnostic client libraries for OpenCensus and export telemetry to common distributed tracing systems such as Zipkin and others.\r\nAlong the way. we will discuss core concepts such as tags, metrics, exporters, zPages and trace context propagation. ", - "startsAt": "2019-02-01T16:20:00", - "endsAt": "2019-02-01T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "9aa1b581-80ab-4510-bb28-d5cede7e6460", - "name": "Simon Zeltser" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10585, - "name": "Microservices" - }, - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10575, - "name": "DevOps" - }, - { - "id": 10572, - "name": "Cross-Platform" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - } - ], - "hasOnlyPlenumSessions": false - }, - { - "id": 2779, - "name": "Room 5", - "sessions": [ - { - "id": "67087", - "title": "Fallacies of Doom - Lessons learned from porting Doom 3 to Java", - "description": "\r\nWhile Java has grown enormously over the years, the fundamentals have stagnated.\r\n\r\nThe motivation for this talk and underlying project, was the following question: why is Java, with it’s 20 years of age, and (at least)6 billion running JVM’s not a major player in the video-game development universe?\r\n\r\n#####TL;DR;\r\nSo everybody knows the Doom games, right? Every new installment brought brand new ideas, and groundbreaking graphics. But more importantly, they brought the source code of the prior installment to the public eye.\r\n\r\nNaturally people have played and hacked the code to oblivion, as much as they played the games themselves. And I have the honor to be one of those people.\r\n\r\nI (naively) endeavored to port the Doom 3 C++ code to the fantabulous Java. In doing so, I hoped to learn, among other things, more about 3D graphics.\r\n...what I didn't expect though, was for djoom3(cool name huh!) to teach me more about Java!?\r\n\r\nAside from the basic game development intro, this talk focuses on the following:\r\n- Some areas where Java should learn from it's nemesis, C++\r\n- Other areas where the student(Java) becomes the master(C++)\r\n- And some promises that were made, but never keptWhile Ja", - "startsAt": "2019-02-01T10:20:00", - "endsAt": "2019-02-01T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "981b5372-9139-4fca-90ee-020fba2dafdc", - "name": "Mahmoud Abdelghany" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10568, - "name": "C++" - }, - { - "id": 10579, - "name": "Fun" - }, - { - "id": 10583, - "name": "Languages" - }, - { - "id": 10602, - "name": "Gaming" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - { - "id": "68282", - "title": "Just what is a \"service mesh\", and if I get one will it make everything OK?", - "description": "Communication is the backbone of distributed applications. Imagine you could control that backbone independently of all the components, so your application code just makes simple calls to other services, and your communication backbone does all the complex non-functional work. Load balancing, traffic management, fault tolerance, end-to-end monitoring, dynamic routing and secure communication could all be applied and controlled centrally. That's a service mesh.\r\n\r\nIn this session I'll cover the major features of a service mesh using Istio - which is the most popular technology in this space. I'll show you what you can do with a service mesh, how it simplifies application design and also where it adds complexity. My examples will range from new microservices designs to legacy monoliths, and you'll see what a service mesh can bring to different types of application.", - "startsAt": "2019-02-01T11:40:00", - "endsAt": "2019-02-01T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "e48f1ee5-93ea-4113-a4ca-5de7b129c3ae", - "name": "Elton Stoneman" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10585, - "name": "Microservices" - }, - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10576, - "name": "Docker" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - { - "id": "58138", - "title": "Kubernetes - going beyond the basics", - "description": "In this talk and with demos we'll cover some more advanced topics within Kubernetes such as:-\r\nInfluencing the scheduling of pods\r\nControlling applications being scheduled using admission controllers\r\nAuto-scaling of applications and clusters\r\nOptions for extending/customising Kubernetes using Custom Resources\r\nAdding a service mesh to improve traffic shaping\r\n \r\nAfter this talk attendees should have a much clearer understanding of the additional capability in Kubernetes and associated platforms that they can to use to improve their application platform.\r\n \r\nAttendees should have a good understanding of the basic Kubernetes concepts and constructs.", - "startsAt": "2019-02-01T13:40:00", - "endsAt": "2019-02-01T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "fb14f65c-538d-4980-9010-8ffbd2d8d4b5", - "name": "Shahid Iqbal" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10575, - "name": "DevOps" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - { - "id": "67372", - "title": "3D printed Bionic Hand a little IOT and a Xamarin Mobile App", - "description": "Kayden a local 13yr Old was born with no left forearm and hand, being continually let down by very poor and expensive NHS prosthetics. Being a close friend of the family, I started looking around the web after seeing news reports of home-made devices. I stumbled across the OpenBionics team and the great work they do and set to building a version for Kayden. \r\nAfter 3D printing the parts I moved onto the electronics but this requires building a bespoke board that often needs software changes to set-up and configure. So my version uses off the shelf IOT parts and connects via Bluetooth to a Xamarin .Net application for changing the settings and configuring on Kayden’s phone. Still a work in progress but I will talk about the process used to get this far and how I have hopefully reduced the costs even more from a few hundred to around £80 for future hands and plan to give back to the opensource project.", - "startsAt": "2019-02-01T15:00:00", - "endsAt": "2019-02-01T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "3a76506a-5c6c-4118-937b-6f7b6eefa975", - "name": "Clifford Agius" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10604, - "name": "3D Modeling" - }, - { - "id": 10606, - "name": "Embedded" - }, - { - "id": 10579, - "name": "Fun" - }, - { - "id": 10588, - "name": "People" - }, - { - "id": 10587, - "name": "Mobile" - }, - { - "id": 10581, - "name": "IoT" - }, - { - "id": 10574, - "name": "Design" - }, - { - "id": 10572, - "name": "Cross-Platform" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - { - "id": "66791", - "title": "Kotlin for C# Developers", - "description": "Dive into the latest craze in languages and platforms - Kotlin. This time we will be looking at it from the perspective of a .NET C# developer, draw comparisons between the languages, and bridge the gap between these 2 amazing languages.\r\n\r\nWe'll look at:\r\n- Kotlin as a language\r\n- Platforms Kotlin is great for\r\n- Object Oriented Implementations in Kotlin\r\n- Extended Features\r\n- Features Kotlin has that C# doesn't\r\n- A demo Android application in Kotlin vs a Xamarin.Android app in C#\r\n\r\nIn the end you will leave with a foundational knowledge of Kotlin and its capabilities to build awesome apps with less code. You should feel comfortable comparing C# applications to Kotlin applications and know where to find resources to learn even more!", - "startsAt": "2019-02-01T16:20:00", - "endsAt": "2019-02-01T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "42b758e4-1a7c-434d-9951-ddff53503d1f", - "name": "Alex Dunn" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10583, - "name": "Languages" - }, - { - "id": 10587, - "name": "Mobile" - }, - { - "id": 10580, - "name": "Functional Programming" - }, - { - "id": 10593, - "name": "Tools" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - } - ], - "hasOnlyPlenumSessions": false - }, - { - "id": 2780, - "name": "Room 6", - "sessions": [ - { - "id": "76427", - "title": "Lessons learned building ASP.NET Core MVC", - "description": "Join Ryan Nowak from the ASP.NET Core team as he shares learnings from building the framework, improving performance, and helping customers. We’ll talk about design details of Razor Pages and Controllers for Web and APIs, as well as hidden gems and power user features. Come learn how MVC works and get some useful tips from one of the core developers.", - "startsAt": "2019-02-01T10:20:00", - "endsAt": "2019-02-01T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "cdbc7319-2548-46b7-9ba8-e8c1e299465e", - "name": "Ryan Nowak" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - { - "id": "69938", - "title": "The promise of an async future awaits", - "description": "The prominent trends in software a distributed programs powered by cooperating microservices, each operating independently. These distributed systems are asynchronous by their very nature. You will use asynchronous programming paradigms to build these systesms.\r\n\r\nIn this session, you'll see the most common mistakes developers make using async and await in C#. You'll see the practices you should use instead. This session also provides a deep dive into async streams, a new feature introduced in C# 8.\r\n\r\nYou're building asynchronous programs now. Make sure you're building them for the future.", - "startsAt": "2019-02-01T15:00:00", - "endsAt": "2019-02-01T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "be2a42b5-fc04-47c8-860b-7c503a8c6854", - "name": "Bill Wagner" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - { - "id": "68532", - "title": "A Piece of Git", - "description": "You use Git, and maybe you even know the internals: all those blobs, trees, commits and refs make it look like Git is sane, well-designed and organized system. But is it, really?\r\n\r\nAfter all, why are there three different kinds of rebase? What makes merge recursive? And what's the deal with line ending normalization? Edward Thomson shows off some of the weirder idiosyncrasies in Git and why it works the way it does.", - "startsAt": "2019-02-01T16:20:00", - "endsAt": "2019-02-01T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "fee375b8-047c-4ea2-ab43-9837f2420b19", - "name": "Edward Thomson" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10575, - "name": "DevOps" - } - ], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - } - ], - "hasOnlyPlenumSessions": false - } - ], - "timeSlots": [ - { - "slotStart": "09:00:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "69232", - "title": "Keynote: The Microsoft Open Source Cinematic Universe - Phase 2", - "description": "Phase 1 is nearly complete with open source .NET Core. What does Microsoft's Open Source plan look like for the next 10 years?\r\n\r\nJoin Scott Hanselman as he compares the MCU (Marvel Cinematic Universe) to the MSFTOSSCU and talks about what a next phase MIGHT look like.", - "startsAt": "2019-02-01T09:00:00", - "endsAt": "2019-02-01T10:00:00", - "isServiceSession": false, - "isPlenumSession": true, - "speakers": [ - { - "id": "142dd289-d6c7-4385-9f36-05b816ceea2c", - "name": "Scott Hanselman" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - } - ] - }, - { - "slotStart": "10:20:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "69004", - "title": "Async injection", - "description": "This talk attempts to answer a pair of frequently asked questions, the first one of which is: how do I combine dependency injection with async and await in C# without leaky abstractions?\r\n\r\nIt turns out that the answer to that question can be found by answering another frequently asked question: how do I get the value out of my monad?\r\n\r\nDuring the talk, you’ll get a quick and easy-to-understand explanation of monads.\r\n\r\nAll code examples will be in C#.", - "startsAt": "2019-02-01T10:20:00", - "endsAt": "2019-02-01T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "84f7d361-4388-44ad-bc48-31695762bc6d", - "name": "Mark Seemann" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10580, - "name": "Functional Programming" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "69239", - "title": "Ctrl-Alt-Del: Learning to Love Legacy Code", - "description": "The world runs on legacy code. For every greenfield progressive web app with 100% test coverage, there are literally hundreds of archaic line-of-business applications running in production - systems with no tests, no documentation, built using out-of-date tools, languages and platforms. It’s the code developers love to hate: it’s not exciting, it’s not shiny, and it won’t look good on your CV - but the world runs on legacy code, and, as developers, if we’re going to work on anything that actually matters, we’re going to end up dealing with legacy. To work effectively with this kind of system, we need to answer some fundamental questions: why was it built this way in the first place? What's happened over the years it's been running in production? And, most importantly, how can we develop our understanding of legacy codebases to the point where we're confident that we can add features, fix bugs and improve performance without making things worse?\r\n\r\nDylan worked on the web application stack at Spotlight (www.spotlight.com) from 2000 until 2018 - first as a supplier, then as webmaster, then as systems architect. Working on the same codebase for nearly two decades has given him an unusual perspective on how applications go from being cutting-edge to being 'legacy'. In this talk, he'll share tips, patterns and techniques that he's learned from helping new developers work with a large and unfamiliar codebase. We'll talk about virtualisation, refactoring tools, and how to bring legacy code under control using continuous integration and managed deployments. We'll explore creative ways to use common technologies like DNS to create more productive development environments. We'll talk about how to bridge the gap between automated testing and systems monitoring, how to improve visibility and transparency of your production systems - and why good old Ctrl-Alt-Del might be the secret to unlocking the potential of your legacy codebase.", - "startsAt": "2019-02-01T10:20:00", - "endsAt": "2019-02-01T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "1d7dcbfc-1de6-4228-8bd6-04f4ba1c4267", - "name": "Dylan Beattie" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10571, - "name": "Continuous Delivery" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "69231", - "title": "C# 8", - "description": "It's nearly here! The Visual Studio 2019 Preview is already available, so if you haven't looked into what's coming in C# 8, now is the perfect time to do so.\r\nThe most important feature of C# 8 is undoubtedly nullable reference types, but there's plenty more to look forward to as well.\r\n\r\nWhile I'll make this talk as easy to understand as I can, there's a huge amount to cover. Expect a fast pace, with lots of code.", - "startsAt": "2019-02-01T10:20:00", - "endsAt": "2019-02-01T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "1176547b-89c5-40ee-b7d7-bf0eb5ca0aef", - "name": "Jon Skeet" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "58161", - "title": "Making Accessibility Testing Suck Less: An Intro to Pa11y.", - "description": "Often the hardest part of any problem is simply how to get started. On the ever-evolving web accessibility is a matter of ongoing importance: the brilliance of your code or sleekness of your UI is inconsequential if your app or website is unusable to some of your users. With a million other issues already on your plate how do you find a way to get started on accessibility testing? Pa11y to the rescue! Pa11y is a lightweight command-line accessibility testing tool with enough flexibility to integrate results into your current testing process. This talk will explain what pa11y does and does not cover, review examples of both command line and scripted usage, dive into the pa11y web service and show how to modify output to work in your current testing setup. Bonus content: how to convince the rest of your team and business why accessibility is worth prioritizing and how getting started with low-hanging fruit can vastly improve your product.", - "startsAt": "2019-02-01T10:20:00", - "endsAt": "2019-02-01T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "e5b9e6c6-1477-4b19-8347-06683a7417de", - "name": "Jennifer Wadella" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10592, - "name": "Testing" - }, - { - "id": 10582, - "name": "JavaScript" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "67087", - "title": "Fallacies of Doom - Lessons learned from porting Doom 3 to Java", - "description": "\r\nWhile Java has grown enormously over the years, the fundamentals have stagnated.\r\n\r\nThe motivation for this talk and underlying project, was the following question: why is Java, with it’s 20 years of age, and (at least)6 billion running JVM’s not a major player in the video-game development universe?\r\n\r\n#####TL;DR;\r\nSo everybody knows the Doom games, right? Every new installment brought brand new ideas, and groundbreaking graphics. But more importantly, they brought the source code of the prior installment to the public eye.\r\n\r\nNaturally people have played and hacked the code to oblivion, as much as they played the games themselves. And I have the honor to be one of those people.\r\n\r\nI (naively) endeavored to port the Doom 3 C++ code to the fantabulous Java. In doing so, I hoped to learn, among other things, more about 3D graphics.\r\n...what I didn't expect though, was for djoom3(cool name huh!) to teach me more about Java!?\r\n\r\nAside from the basic game development intro, this talk focuses on the following:\r\n- Some areas where Java should learn from it's nemesis, C++\r\n- Other areas where the student(Java) becomes the master(C++)\r\n- And some promises that were made, but never keptWhile Ja", - "startsAt": "2019-02-01T10:20:00", - "endsAt": "2019-02-01T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "981b5372-9139-4fca-90ee-020fba2dafdc", - "name": "Mahmoud Abdelghany" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10568, - "name": "C++" - }, - { - "id": 10579, - "name": "Fun" - }, - { - "id": 10583, - "name": "Languages" - }, - { - "id": 10602, - "name": "Gaming" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - }, - { - "id": 2780, - "name": "Room 6", - "session": { - "id": "76427", - "title": "Lessons learned building ASP.NET Core MVC", - "description": "Join Ryan Nowak from the ASP.NET Core team as he shares learnings from building the framework, improving performance, and helping customers. We’ll talk about design details of Razor Pages and Controllers for Web and APIs, as well as hidden gems and power user features. Come learn how MVC works and get some useful tips from one of the core developers.", - "startsAt": "2019-02-01T10:20:00", - "endsAt": "2019-02-01T11:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "cdbc7319-2548-46b7-9ba8-e8c1e299465e", - "name": "Ryan Nowak" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - "index": 5 - } - ] - }, - { - "slotStart": "11:40:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "68521", - "title": "From 'dotnet run' to 'Hello World!'", - "description": "Have you ever stopped to think about all the things that happen when you execute a simple .NET program?\r\n\r\nThis talk will delve into the internals of the recently open-sourced .NET Core runtime, looking at what happens, when it happens and why. \r\n\r\nMaking use of freely available tools such as 'PerfView', we'll examine the Execution Engine, Type Loader, Just-in-Time (JIT) Compiler and the CLR Hosting API to see how all these components play a part in making 'Hello World' possible.", - "startsAt": "2019-02-01T11:40:00", - "endsAt": "2019-02-01T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "fb89e2c6-39b1-4ef8-8932-609504cf4901", - "name": "Matt Warren" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "67993", - "title": "The Functional Programmer's Toolkit", - "description": "The functional programming community has a number of patterns with strange names such as monads, monoids, functors, and catamorphisms.\r\n \r\nIn this beginner-friendly talk, we'll demystify these techniques and see how they all fit together into a small but versatile \"tool kit\". \r\n\r\nWe'll then see how the tools in this tool kit can be applied to a wide variety of programming problems, such as handling missing data, working with lists, and implementing the functional equivalent of dependency injection. ", - "startsAt": "2019-02-01T11:40:00", - "endsAt": "2019-02-01T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "89335661-3092-4afa-b53f-3d203cafa2a1", - "name": "Scott Wlaschin" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10580, - "name": "Functional Programming" - }, - { - "id": 10574, - "name": "Design" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "67078", - "title": "APIs Exposed!", - "description": "More and more developers are building APIs, whether that be for consumption by client-side applications, exposing endpoints directly to customers so they can use an alternative front-end or wrapping up services in containers.\r\n\r\nNow that we have all these exposed endpoints, what are we doing to secure them? Previously, our monolith was self-contained with limited points of access making authentication and authorisation more straightforward - that’s no longer the case.\r\n\r\nWe’ll cover the potential risks we may face such as cross-site scripting and BruteForce attacks as well as a look at the possible options for securing API endpoints including OAuth, Access Tokens, JSON web tokens, IP whitelisting, rate limiting to name but a few.\r\n\r\n", - "startsAt": "2019-02-01T11:40:00", - "endsAt": "2019-02-01T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "ac105033-2b20-4b45-8d47-5647d8accf84", - "name": "Layla Porter" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10589, - "name": "Security" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "62532", - "title": "A Developer's Guide to Advertising", - "description": "Love it or hate it, advertising is currently a main source of revenue for many websites. But digital advertising is a complicated space that not many developers understand. If you'd like to understand the ad code on your site better, if your ad ops team has been talking about header bidding, or if you're just curious about how ads work, this is the talk for you. I'll give an overview of how programmatic advertising works, what header bidding is, explain some common tools and libraries, and address some common misconceptions and complaints about ads.", - "startsAt": "2019-02-01T11:40:00", - "endsAt": "2019-02-01T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "b8f3c690-5950-4279-9ee7-9291d1439dd2", - "name": "Krista LaFentres" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10596, - "name": "Web" - }, - { - "id": 10582, - "name": "JavaScript" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "68282", - "title": "Just what is a \"service mesh\", and if I get one will it make everything OK?", - "description": "Communication is the backbone of distributed applications. Imagine you could control that backbone independently of all the components, so your application code just makes simple calls to other services, and your communication backbone does all the complex non-functional work. Load balancing, traffic management, fault tolerance, end-to-end monitoring, dynamic routing and secure communication could all be applied and controlled centrally. That's a service mesh.\r\n\r\nIn this session I'll cover the major features of a service mesh using Istio - which is the most popular technology in this space. I'll show you what you can do with a service mesh, how it simplifies application design and also where it adds complexity. My examples will range from new microservices designs to legacy monoliths, and you'll see what a service mesh can bring to different types of application.", - "startsAt": "2019-02-01T11:40:00", - "endsAt": "2019-02-01T12:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "e48f1ee5-93ea-4113-a4ca-5de7b129c3ae", - "name": "Elton Stoneman" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10585, - "name": "Microservices" - }, - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10576, - "name": "Docker" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - } - ] - }, - { - "slotStart": "13:40:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "69233", - "title": "Solving Diabetes with an Open Source Artificial Pancreas", - "description": "Scott has been a Type 1 diabetic for over 20 years. When he first became diabetic he did what every engineer would do...he wrote an app to solve his problem. Fast forward to 2018 and Scott lives 24 hours a day connected to an open source artificial pancreas. After years of waiting, the diabetes community online creating solutions.\r\n\r\nScott will go through the history of diabetes online, the components (both hardware and software) needed for an artificial pancreas, and discuss the architectural design of two popular systems (LoopKit and OpenAPS). Plus, you'll see Scott *not die* live on stage as he's been \"looping\" for over a year!", - "startsAt": "2019-02-01T13:40:00", - "endsAt": "2019-02-01T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "142dd289-d6c7-4385-9f36-05b816ceea2c", - "name": "Scott Hanselman" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "69230", - "title": "Society (n+1).0: smashing the patriarchy and other ways of changing the world", - "description": "Software engineers are fond of casually mentioning how their work changes the world. But have you thought about what you want the world to look like after you've changed it? Do you want to smash the patriarchy [1]? Save the planet? Make Furbies cool again? Is technology really the only tool at your disposal?\r\nThis session is a call to both discovery and action. You already care about Furbies, but what do you not know you don't know about them? Have you looked at Furbies from someone else's perspective? Once your eyes are wide open, what's the next step? How would a Furby smash the patriarchy? Let's learn to be better together, and make a difference in the world today.\r\n\r\nFootnote 1: Our system of society norms which puts everyone in boxes that are hard to escape [2]. The boxes surrounding straight white men can be extremely comfortable ones, but more limiting than you might realize.\r\n\r\nFootnote 2: Furbies are pretty easy to get out of boxes; humans, not so much.", - "startsAt": "2019-02-01T13:40:00", - "endsAt": "2019-02-01T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "1176547b-89c5-40ee-b7d7-bf0eb5ca0aef", - "name": "Jon Skeet" - }, - { - "id": "e5b9e6c6-1477-4b19-8347-06683a7417de", - "name": "Jennifer Wadella" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "69366", - "title": "Architectural patterns for the cloud", - "description": "As more and more people are starting to deploy their application to the cloud, new architectural patterns have emerged, and some old ones have resurfaced or become more prominent.\r\n\r\nIn this session, Mahesh Krishnan, will run through a large catalogue of cloud patterns that will cover categories such as availability, resiliency, data management, performance, scalability, design and implementation. You will learn about what problem each and every pattern addresses, how to implement them and what considerations you need to take into account while implementing them.", - "startsAt": "2019-02-01T13:40:00", - "endsAt": "2019-02-01T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "648e2e9d-28cc-47ef-828c-75a7d599d48d", - "name": "Mahesh Krishnan" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10569, - "name": "Cloud" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "68894", - "title": "\"Hey Mycroft\": Getting Started with the OSS Home Assistant", - "description": "Home virtual assistants, like Alexa, Google Now, Siri, and Cortana, are gaining a lot of popularity. They’re now incorporated into our phones, our laptops, and even available as separate devices in our homes. Some people haven’t adopted them out of privacy concerns. A new system called Mycroft has come onto the scene, and it’s built on open source hardware and software. You can install it on a Raspberry Pi, an old Linux box, or buy their own Mycroft device.\r\n\r\nIn this session, we’ll go over the basics of what Mycroft is, and how you can quickly install it yourself. From there, we’ll talk about some of the underlying software and see a short demo. Finally, we’ll see how to build a new skill into it and contribute it back to the community. You’ll leave with your own virtual assistant and the knowledge on how make it do what you want but keep your privacy in check.", - "startsAt": "2019-02-01T13:40:00", - "endsAt": "2019-02-01T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "97e1c796-8bf6-468b-b246-0efc6979b9e7", - "name": "Sarah Withee" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10581, - "name": "IoT" - }, - { - "id": 10565, - "name": "AI" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "58138", - "title": "Kubernetes - going beyond the basics", - "description": "In this talk and with demos we'll cover some more advanced topics within Kubernetes such as:-\r\nInfluencing the scheduling of pods\r\nControlling applications being scheduled using admission controllers\r\nAuto-scaling of applications and clusters\r\nOptions for extending/customising Kubernetes using Custom Resources\r\nAdding a service mesh to improve traffic shaping\r\n \r\nAfter this talk attendees should have a much clearer understanding of the additional capability in Kubernetes and associated platforms that they can to use to improve their application platform.\r\n \r\nAttendees should have a good understanding of the basic Kubernetes concepts and constructs.", - "startsAt": "2019-02-01T13:40:00", - "endsAt": "2019-02-01T14:40:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "fb14f65c-538d-4980-9010-8ffbd2d8d4b5", - "name": "Shahid Iqbal" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10575, - "name": "DevOps" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - } - ] - }, - { - "slotStart": "15:00:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "69010", - "title": "Crash, Burn, Report", - "description": "With the launch of the Reporting API any browser that visits your site can automatically detect and alert you to a whole heap of problems with your application. DNS not resolving? Serving an invalid certificate? Got a redirect loop, using a soon to be deprecated API or any one of countless other problems, they can all be detected and reported with no user action, no agents, no code to deploy. You have one of the most extensive and powerful monitoring platforms in existence at your disposal, millions of browsers. Let's look at how to use them.\r\n\r\nIn this talk we'll look at how to configure the browser to send you reports when things go wrong. These are brand new capabilities the likes of which we've haven't seen before and they're already supported in the world's most popular browser, Google Chrome. We'll look at how to receive reports and how to make use of them after having the browser do the hard work.", - "startsAt": "2019-02-01T15:00:00", - "endsAt": "2019-02-01T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "e9c64362-6194-409c-85da-45c8fd40abd6", - "name": "Scott Helme" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "69307", - "title": "Tabs, spaces and salaries: a data science detective story", - "description": "In data science, sometimes you stumble across an intriguing property in the data. I will tell you a story of a mysterious correlation - from the StackOverflow developer survey it seems that developers who use spaces have higher salaries than those who use tabs. Correlation doesn't mean causation: using spaces won't suddenly increase your salary. But what does it all mean? Follow me into a detective investigation that will show you how to approach complex data science problems. I will show you some of the perils of correlation, model fitting and biases - how they can be dangerous and how to avoid these traps. And you'll also find how profitable your indentation style really is.", - "startsAt": "2019-02-01T15:00:00", - "endsAt": "2019-02-01T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "14fdabdd-3814-4f84-868e-a8178a25c5f8", - "name": "Evelina Gabasova" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10584, - "name": "Machine Learning" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "66814", - "title": "Unity 101 for C# Developers", - "description": "If you are interested in mixed or virtual reality development then Unity is a tool you’ll want to learn. Go to Microsoft’s [Hololens development page](https://developer.microsoft.com/en-us/windows/mixed-reality/install_the_tools) and you’ll find the installation of Unity recommended in the first paragraph.\r\n\r\nOne surprise to developers getting started with Unity is the level of integration with Visual Studio and how easy it is to add C# to manipulate your 3D world.\r\n\r\nIn this demo led session we’ll go back to basics with Unity, easily creating a simple 3D experience with realistic physics. We’ll look at the fantastic Visual Studio integration and how easily the two editors work together.\r\n\r\nFinally we’ll look at the ways in which we can give our 3D experience some polish by adding textures, animations and some explosions!\r\n\r\nKeeping the best news for last: Unity is royalty free until you’re earning $100,000 – so if you like the idea of becoming wildly rich from making immersive 3D experiences this is the talk for you.", - "startsAt": "2019-02-01T15:00:00", - "endsAt": "2019-02-01T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "ef4df68d-9bdc-471d-aff0-075c77c4a424", - "name": "Andy Clarke" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10603, - "name": "VR" - }, - { - "id": 10604, - "name": "3D Modeling" - }, - { - "id": 10602, - "name": "Gaming" - }, - { - "id": 10579, - "name": "Fun" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "68882", - "title": "Megahertz, Gigahertz, Registers and Instructions: How does a CPU actually work?", - "description": "For decades, we’ve been creating ever higher abstractions between ourselves and the computing hardware we’re programming, but in the end whether you’re writing JavaScript, Haskell, or Python it all comes down to 1’s and 0’s running through hardware patterns that were well understood twenty years ago.\r\n\r\nWe’ll walk through the fundamentals of how CPU’s “think” in an accessible way (no engineering degree required!) so you can appreciate the marvel that is the modern CPU be it in a server data center or your fridge at home. You’ll learn how a CPU turns the code we feed it into actions, what’s the big difference between an ARM and an Intel processor, how CPU’s constantly optimize work for us, and where is it all going for the next few years.\r\n\r\nFinally, we'll show how Meltdown and Spectre take advantage of CPU internals to expose data and why this class of security problems are going to be a challenge to the next generation of CPU's.\r\n", - "startsAt": "2019-02-01T15:00:00", - "endsAt": "2019-02-01T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "fe60954c-9d1a-4167-9faa-77027c8d446b", - "name": "Kendall Miller" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "67372", - "title": "3D printed Bionic Hand a little IOT and a Xamarin Mobile App", - "description": "Kayden a local 13yr Old was born with no left forearm and hand, being continually let down by very poor and expensive NHS prosthetics. Being a close friend of the family, I started looking around the web after seeing news reports of home-made devices. I stumbled across the OpenBionics team and the great work they do and set to building a version for Kayden. \r\nAfter 3D printing the parts I moved onto the electronics but this requires building a bespoke board that often needs software changes to set-up and configure. So my version uses off the shelf IOT parts and connects via Bluetooth to a Xamarin .Net application for changing the settings and configuring on Kayden’s phone. Still a work in progress but I will talk about the process used to get this far and how I have hopefully reduced the costs even more from a few hundred to around £80 for future hands and plan to give back to the opensource project.", - "startsAt": "2019-02-01T15:00:00", - "endsAt": "2019-02-01T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "3a76506a-5c6c-4118-937b-6f7b6eefa975", - "name": "Clifford Agius" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10604, - "name": "3D Modeling" - }, - { - "id": 10606, - "name": "Embedded" - }, - { - "id": 10579, - "name": "Fun" - }, - { - "id": 10588, - "name": "People" - }, - { - "id": 10587, - "name": "Mobile" - }, - { - "id": 10581, - "name": "IoT" - }, - { - "id": 10574, - "name": "Design" - }, - { - "id": 10572, - "name": "Cross-Platform" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - }, - { - "id": 2780, - "name": "Room 6", - "session": { - "id": "69938", - "title": "The promise of an async future awaits", - "description": "The prominent trends in software a distributed programs powered by cooperating microservices, each operating independently. These distributed systems are asynchronous by their very nature. You will use asynchronous programming paradigms to build these systesms.\r\n\r\nIn this session, you'll see the most common mistakes developers make using async and await in C#. You'll see the practices you should use instead. This session also provides a deep dive into async streams, a new feature introduced in C# 8.\r\n\r\nYou're building asynchronous programs now. Make sure you're building them for the future.", - "startsAt": "2019-02-01T15:00:00", - "endsAt": "2019-02-01T16:00:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "be2a42b5-fc04-47c8-860b-7c503a8c6854", - "name": "Bill Wagner" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - "index": 5 - } - ] - }, - { - "slotStart": "16:20:00", - "rooms": [ - { - "id": 2775, - "name": "Room 1", - "session": { - "id": "66952", - "title": "Futurology for Dummies - the Next 30 Years in Tech", - "description": "2019 is the 30th anniversary of my first job in tech. On my first day I was given a Wyse 60 terminal attached via RS232 cables to a Tandon 286, and told to learn C from a dead tree so I could write text applications for an 80x24 character screen. Fast-forward to now: my phone is about a million times more powerful than that Tandon; screens are 3840x2160 pixels; every computer in the world is attached to every other thing with no cables; and we code using, well, still basically C.\r\n\r\nHaving lived through all those changes in realtime, and as an incurable neophile, I think I can make an educated guess as to what the next 30 years are going to be like, and what we're all going to be doing by 2049. If anything, I'm going to underestimate it, but hopefully you'll be inspired, invigorated and maybe even informed about the future of your career in tech.", - "startsAt": "2019-02-01T16:20:00", - "endsAt": "2019-02-01T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "70590ad0-b1b3-40b8-b05d-b58722ef9d9d", - "name": "Mark Rendle" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10579, - "name": "Fun" - }, - { - "id": 10584, - "name": "Machine Learning" - }, - { - "id": 10581, - "name": "IoT" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10565, - "name": "AI" - }, - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10594, - "name": "UI" - }, - { - "id": 10595, - "name": "UX" - }, - { - "id": 10596, - "name": "Web" - }, - { - "id": 10574, - "name": "Design" - }, - { - "id": 10587, - "name": "Mobile" - }, - { - "id": 10604, - "name": "3D Modeling" - }, - { - "id": 10603, - "name": "VR" - } - ], - "sort": 4 - } - ], - "roomId": 2775, - "room": "Room 1" - }, - "index": 0 - }, - { - "id": 2776, - "name": "Room 2", - "session": { - "id": "67994", - "title": "Four Languages from Forty Years Ago", - "description": "The 1970's were a golden age for new programming languages, but do they have any relevance to programming today? Can we still learn from them?\r\n\r\nIn this talk, we'll look at four languages designed over forty years ago -- SQL, Prolog, ML, and Smalltalk -- and discuss their philosophy and approach to programming, which is very different from most popular languages today.\r\n\r\nWe'll come away with some practical principles that are still very applicable to modern development. And you might discover your new favorite programming paradigm!\r\n", - "startsAt": "2019-02-01T16:20:00", - "endsAt": "2019-02-01T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "89335661-3092-4afa-b53f-3d203cafa2a1", - "name": "Scott Wlaschin" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10583, - "name": "Languages" - } - ], - "sort": 4 - } - ], - "roomId": 2776, - "room": "Room 2" - }, - "index": 1 - }, - { - "id": 2777, - "name": "Room 3", - "session": { - "id": "67138", - "title": "Deep Learning in the world of little ponies", - "description": "In this talk, we will discuss computer vision, one of the most common real-world applications of machine learning. We will deep dive into various state-of-the-art concepts around building custom image classifiers - application of deep neural networks, specifically convolutional neural networks and transfer learning. We will demonstrate how those approaches could be used to create your own image classifier to recognise the characters of \"My Little Pony\" TV Series [or Pokemon, or Superheroes, or your custom images].", - "startsAt": "2019-02-01T16:20:00", - "endsAt": "2019-02-01T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "b4c506ac-9757-4ac6-8b7c-3d6696b113e4", - "name": "Galiya Warrier" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10584, - "name": "Machine Learning" - }, - { - "id": 10579, - "name": "Fun" - }, - { - "id": 10565, - "name": "AI" - } - ], - "sort": 4 - } - ], - "roomId": 2777, - "room": "Room 3" - }, - "index": 2 - }, - { - "id": 2778, - "name": "Room 4", - "session": { - "id": "67872", - "title": "Distributed Tracing and Monitoring with OpenCensus", - "description": "OpenCensus is an emerging standard for tracing and metrics of cloud services. You can use it to gain observability into applications that span multiple clouds and technological stacks.\r\nIn this talk, we will use open source and vendor agnostic client libraries for OpenCensus and export telemetry to common distributed tracing systems such as Zipkin and others.\r\nAlong the way. we will discuss core concepts such as tags, metrics, exporters, zPages and trace context propagation. ", - "startsAt": "2019-02-01T16:20:00", - "endsAt": "2019-02-01T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "9aa1b581-80ab-4510-bb28-d5cede7e6460", - "name": "Simon Zeltser" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10585, - "name": "Microservices" - }, - { - "id": 10566, - "name": "Architecture" - }, - { - "id": 10575, - "name": "DevOps" - }, - { - "id": 10572, - "name": "Cross-Platform" - }, - { - "id": 10569, - "name": "Cloud" - }, - { - "id": 10563, - "name": ".NET" - } - ], - "sort": 4 - } - ], - "roomId": 2778, - "room": "Room 4" - }, - "index": 3 - }, - { - "id": 2779, - "name": "Room 5", - "session": { - "id": "66791", - "title": "Kotlin for C# Developers", - "description": "Dive into the latest craze in languages and platforms - Kotlin. This time we will be looking at it from the perspective of a .NET C# developer, draw comparisons between the languages, and bridge the gap between these 2 amazing languages.\r\n\r\nWe'll look at:\r\n- Kotlin as a language\r\n- Platforms Kotlin is great for\r\n- Object Oriented Implementations in Kotlin\r\n- Extended Features\r\n- Features Kotlin has that C# doesn't\r\n- A demo Android application in Kotlin vs a Xamarin.Android app in C#\r\n\r\nIn the end you will leave with a foundational knowledge of Kotlin and its capabilities to build awesome apps with less code. You should feel comfortable comparing C# applications to Kotlin applications and know where to find resources to learn even more!", - "startsAt": "2019-02-01T16:20:00", - "endsAt": "2019-02-01T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "42b758e4-1a7c-434d-9951-ddff53503d1f", - "name": "Alex Dunn" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10563, - "name": ".NET" - }, - { - "id": 10583, - "name": "Languages" - }, - { - "id": 10587, - "name": "Mobile" - }, - { - "id": 10580, - "name": "Functional Programming" - }, - { - "id": 10593, - "name": "Tools" - } - ], - "sort": 4 - } - ], - "roomId": 2779, - "room": "Room 5" - }, - "index": 4 - }, - { - "id": 2780, - "name": "Room 6", - "session": { - "id": "68532", - "title": "A Piece of Git", - "description": "You use Git, and maybe you even know the internals: all those blobs, trees, commits and refs make it look like Git is sane, well-designed and organized system. But is it, really?\r\n\r\nAfter all, why are there three different kinds of rebase? What makes merge recursive? And what's the deal with line ending normalization? Edward Thomson shows off some of the weirder idiosyncrasies in Git and why it works the way it does.", - "startsAt": "2019-02-01T16:20:00", - "endsAt": "2019-02-01T17:20:00", - "isServiceSession": false, - "isPlenumSession": false, - "speakers": [ - { - "id": "fee375b8-047c-4ea2-ab43-9837f2420b19", - "name": "Edward Thomson" - } - ], - "categories": [ - { - "id": 2223, - "name": "Tags", - "categoryItems": [ - { - "id": 10593, - "name": "Tools" - }, - { - "id": 10575, - "name": "DevOps" - } - ], - "sort": 4 - } - ], - "roomId": 2780, - "room": "Room 6" - }, - "index": 5 - } - ] - } - ] - } -] \ No newline at end of file diff --git a/code/complete/GraphQL/Migrations/20200725133424_Initial.Designer.cs b/code/complete/GraphQL/Migrations/20200725133424_Initial.Designer.cs deleted file mode 100644 index 94e8f17..0000000 --- a/code/complete/GraphQL/Migrations/20200725133424_Initial.Designer.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -using ConferencePlanner.GraphQL.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace ConferencePlanner.GraphQL.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20200725133424_Initial")] - partial class Initial - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.6"); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bio") - .HasColumnType("TEXT") - .HasMaxLength(4000); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(200); - - b.Property("WebSite") - .HasColumnType("TEXT") - .HasMaxLength(1000); - - b.HasKey("Id"); - - b.ToTable("Speakers"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/code/complete/GraphQL/Migrations/20200725184711_Refactoring.Designer.cs b/code/complete/GraphQL/Migrations/20200725184711_Refactoring.Designer.cs deleted file mode 100644 index 5192ad9..0000000 --- a/code/complete/GraphQL/Migrations/20200725184711_Refactoring.Designer.cs +++ /dev/null @@ -1,194 +0,0 @@ -// -using System; -using ConferencePlanner.GraphQL.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace ConferencePlanner.GraphQL.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20200725184711_Refactoring")] - partial class Refactoring - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.6"); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("EmailAddress") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("FirstName") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(200); - - b.Property("LastName") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(200); - - b.Property("UserName") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(200); - - b.HasKey("Id"); - - b.HasIndex("UserName") - .IsUnique(); - - b.ToTable("Attendees"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Abstract") - .HasColumnType("TEXT") - .HasMaxLength(4000); - - b.Property("EndTime") - .HasColumnType("TEXT"); - - b.Property("StartTime") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(200); - - b.Property("TrackId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TrackId"); - - b.ToTable("Sessions"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => - { - b.Property("SessionId") - .HasColumnType("INTEGER"); - - b.Property("AttendeeId") - .HasColumnType("INTEGER"); - - b.HasKey("SessionId", "AttendeeId"); - - b.HasIndex("AttendeeId"); - - b.ToTable("SessionAttendee"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => - { - b.Property("SessionId") - .HasColumnType("INTEGER"); - - b.Property("SpeakerId") - .HasColumnType("INTEGER"); - - b.HasKey("SessionId", "SpeakerId"); - - b.HasIndex("SpeakerId"); - - b.ToTable("SessionSpeaker"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bio") - .HasColumnType("TEXT") - .HasMaxLength(4000); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(200); - - b.Property("WebSite") - .HasColumnType("TEXT") - .HasMaxLength(1000); - - b.HasKey("Id"); - - b.ToTable("Speakers"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Track", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(200); - - b.HasKey("Id"); - - b.ToTable("Tracks"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => - { - b.HasOne("ConferencePlanner.GraphQL.Data.Track", "Track") - .WithMany("Sessions") - .HasForeignKey("TrackId"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => - { - b.HasOne("ConferencePlanner.GraphQL.Data.Attendee", "Attendee") - .WithMany("SessionsAttendees") - .HasForeignKey("AttendeeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("ConferencePlanner.GraphQL.Data.Session", "Session") - .WithMany("SessionAttendees") - .HasForeignKey("SessionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => - { - b.HasOne("ConferencePlanner.GraphQL.Data.Session", "Session") - .WithMany("SessionSpeakers") - .HasForeignKey("SessionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("ConferencePlanner.GraphQL.Data.Speaker", "Speaker") - .WithMany("SessionSpeakers") - .HasForeignKey("SpeakerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/code/complete/GraphQL/Migrations/20200725184711_Refactoring.cs b/code/complete/GraphQL/Migrations/20200725184711_Refactoring.cs deleted file mode 100644 index a5b7768..0000000 --- a/code/complete/GraphQL/Migrations/20200725184711_Refactoring.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace ConferencePlanner.GraphQL.Migrations -{ - public partial class Refactoring : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Attendees", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - FirstName = table.Column(maxLength: 200, nullable: false), - LastName = table.Column(maxLength: 200, nullable: false), - UserName = table.Column(maxLength: 200, nullable: false), - EmailAddress = table.Column(maxLength: 256, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Attendees", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Tracks", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(maxLength: 200, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Tracks", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Sessions", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Title = table.Column(maxLength: 200, nullable: false), - Abstract = table.Column(maxLength: 4000, nullable: true), - StartTime = table.Column(nullable: true), - EndTime = table.Column(nullable: true), - TrackId = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Sessions", x => x.Id); - table.ForeignKey( - name: "FK_Sessions_Tracks_TrackId", - column: x => x.TrackId, - principalTable: "Tracks", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "SessionAttendee", - columns: table => new - { - SessionId = table.Column(nullable: false), - AttendeeId = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_SessionAttendee", x => new { x.SessionId, x.AttendeeId }); - table.ForeignKey( - name: "FK_SessionAttendee_Attendees_AttendeeId", - column: x => x.AttendeeId, - principalTable: "Attendees", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_SessionAttendee_Sessions_SessionId", - column: x => x.SessionId, - principalTable: "Sessions", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "SessionSpeaker", - columns: table => new - { - SessionId = table.Column(nullable: false), - SpeakerId = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_SessionSpeaker", x => new { x.SessionId, x.SpeakerId }); - table.ForeignKey( - name: "FK_SessionSpeaker_Sessions_SessionId", - column: x => x.SessionId, - principalTable: "Sessions", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_SessionSpeaker_Speakers_SpeakerId", - column: x => x.SpeakerId, - principalTable: "Speakers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Attendees_UserName", - table: "Attendees", - column: "UserName", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_SessionAttendee_AttendeeId", - table: "SessionAttendee", - column: "AttendeeId"); - - migrationBuilder.CreateIndex( - name: "IX_Sessions_TrackId", - table: "Sessions", - column: "TrackId"); - - migrationBuilder.CreateIndex( - name: "IX_SessionSpeaker_SpeakerId", - table: "SessionSpeaker", - column: "SpeakerId"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "SessionAttendee"); - - migrationBuilder.DropTable( - name: "SessionSpeaker"); - - migrationBuilder.DropTable( - name: "Attendees"); - - migrationBuilder.DropTable( - name: "Sessions"); - - migrationBuilder.DropTable( - name: "Tracks"); - } - } -} diff --git a/code/complete/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs b/code/complete/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs deleted file mode 100644 index 64774c3..0000000 --- a/code/complete/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs +++ /dev/null @@ -1,192 +0,0 @@ -// -using System; -using ConferencePlanner.GraphQL.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace ConferencePlanner.GraphQL.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - partial class ApplicationDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "3.1.6"); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("EmailAddress") - .HasColumnType("TEXT") - .HasMaxLength(256); - - b.Property("FirstName") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(200); - - b.Property("LastName") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(200); - - b.Property("UserName") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(200); - - b.HasKey("Id"); - - b.HasIndex("UserName") - .IsUnique(); - - b.ToTable("Attendees"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Abstract") - .HasColumnType("TEXT") - .HasMaxLength(4000); - - b.Property("EndTime") - .HasColumnType("TEXT"); - - b.Property("StartTime") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(200); - - b.Property("TrackId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TrackId"); - - b.ToTable("Sessions"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => - { - b.Property("SessionId") - .HasColumnType("INTEGER"); - - b.Property("AttendeeId") - .HasColumnType("INTEGER"); - - b.HasKey("SessionId", "AttendeeId"); - - b.HasIndex("AttendeeId"); - - b.ToTable("SessionAttendee"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => - { - b.Property("SessionId") - .HasColumnType("INTEGER"); - - b.Property("SpeakerId") - .HasColumnType("INTEGER"); - - b.HasKey("SessionId", "SpeakerId"); - - b.HasIndex("SpeakerId"); - - b.ToTable("SessionSpeaker"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bio") - .HasColumnType("TEXT") - .HasMaxLength(4000); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(200); - - b.Property("WebSite") - .HasColumnType("TEXT") - .HasMaxLength(1000); - - b.HasKey("Id"); - - b.ToTable("Speakers"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Track", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasMaxLength(200); - - b.HasKey("Id"); - - b.ToTable("Tracks"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => - { - b.HasOne("ConferencePlanner.GraphQL.Data.Track", "Track") - .WithMany("Sessions") - .HasForeignKey("TrackId"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => - { - b.HasOne("ConferencePlanner.GraphQL.Data.Attendee", "Attendee") - .WithMany("SessionsAttendees") - .HasForeignKey("AttendeeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("ConferencePlanner.GraphQL.Data.Session", "Session") - .WithMany("SessionAttendees") - .HasForeignKey("SessionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => - { - b.HasOne("ConferencePlanner.GraphQL.Data.Session", "Session") - .WithMany("SessionSpeakers") - .HasForeignKey("SessionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("ConferencePlanner.GraphQL.Data.Speaker", "Speaker") - .WithMany("SessionSpeakers") - .HasForeignKey("SpeakerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/code/complete/GraphQL/Program.cs b/code/complete/GraphQL/Program.cs deleted file mode 100644 index 4919e8a..0000000 --- a/code/complete/GraphQL/Program.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; - -namespace ConferencePlanner.GraphQL -{ - 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/code/complete/GraphQL/Sessions/AddSessionInput.cs b/code/complete/GraphQL/Sessions/AddSessionInput.cs deleted file mode 100644 index 3eb1186..0000000 --- a/code/complete/GraphQL/Sessions/AddSessionInput.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public record AddSessionInput( - string Title, - string? Abstract, - [property: ID(nameof(Speaker))] - IReadOnlyList SpeakerIds); -} \ No newline at end of file diff --git a/code/complete/GraphQL/Sessions/AddSessionPayload.cs b/code/complete/GraphQL/Sessions/AddSessionPayload.cs deleted file mode 100644 index d1fec9f..0000000 --- a/code/complete/GraphQL/Sessions/AddSessionPayload.cs +++ /dev/null @@ -1,20 +0,0 @@ -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class AddSessionPayload : Payload - { - public AddSessionPayload(Session session) - { - Session = session; - } - - public AddSessionPayload(UserError error) - : base(new[] { error }) - { - } - - public Session? Session { get; } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Sessions/RenameSessionInput.cs b/code/complete/GraphQL/Sessions/RenameSessionInput.cs deleted file mode 100644 index 17e87cd..0000000 --- a/code/complete/GraphQL/Sessions/RenameSessionInput.cs +++ /dev/null @@ -1,9 +0,0 @@ -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public record RenameSessionInput( - [property: ID(nameof(Session))] string SessionId, - string Title); -} \ No newline at end of file diff --git a/code/complete/GraphQL/Sessions/RenameSessionPayload.cs b/code/complete/GraphQL/Sessions/RenameSessionPayload.cs deleted file mode 100644 index b9970a7..0000000 --- a/code/complete/GraphQL/Sessions/RenameSessionPayload.cs +++ /dev/null @@ -1,20 +0,0 @@ -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class RenameSessionPayload : Payload - { - public RenameSessionPayload(Session session) - { - Session = session; - } - - public RenameSessionPayload(UserError error) - : base(new[] { error }) - { - } - - public Session? Session { get; } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Sessions/ScheduleSessionInput.cs b/code/complete/GraphQL/Sessions/ScheduleSessionInput.cs deleted file mode 100644 index 0343034..0000000 --- a/code/complete/GraphQL/Sessions/ScheduleSessionInput.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public record ScheduleSessionInput( - [property: ID(nameof(Session))] - int SessionId, - [property: ID(nameof(Track))] - int TrackId, - DateTimeOffset StartTime, - DateTimeOffset EndTime); -} \ No newline at end of file diff --git a/code/complete/GraphQL/Sessions/ScheduleSessionPayload.cs b/code/complete/GraphQL/Sessions/ScheduleSessionPayload.cs deleted file mode 100644 index 057ad6e..0000000 --- a/code/complete/GraphQL/Sessions/ScheduleSessionPayload.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class ScheduleSessionPayload : SessionPayloadBase - { - public ScheduleSessionPayload(Session session) - : base(session) - { - } - - public ScheduleSessionPayload(UserError error) - : base(new[] { error }) - { - } - - public async Task GetTrackAsync( - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) - { - if (Session is null) - { - return null; - } - - return await trackById.LoadAsync(Session.Id, cancellationToken); - } - - [UseApplicationDbContext] - public async Task?> GetSpeakersAsync( - [ScopedService] ApplicationDbContext dbContext, - SpeakerByIdDataLoader speakerById, - CancellationToken cancellationToken) - { - if (Session is null) - { - return null; - } - - int[] speakerIds = await dbContext.Sessions - .Where(s => s.Id == Session.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SpeakerId)) - .ToArrayAsync(cancellationToken); - - return await speakerById.LoadAsync(speakerIds, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Sessions/SessionFilterInputType.cs b/code/complete/GraphQL/Sessions/SessionFilterInputType.cs deleted file mode 100644 index b1dd3e6..0000000 --- a/code/complete/GraphQL/Sessions/SessionFilterInputType.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Data.Filters; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class SessionFilterInputType : FilterInputType - { - protected override void Configure(IFilterInputTypeDescriptor descriptor) - { - descriptor.Ignore(t => t.Id); - descriptor.Ignore(t => t.TrackId); - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Sessions/SessionMutations.cs b/code/complete/GraphQL/Sessions/SessionMutations.cs deleted file mode 100644 index 63c933b..0000000 --- a/code/complete/GraphQL/Sessions/SessionMutations.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Subscriptions; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Sessions -{ - [ExtendObjectType(OperationTypeNames.Mutation)] - public class SessionMutations - { - [UseApplicationDbContext] - public async Task AddSessionAsync( - AddSessionInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(input.Title)) - { - return new AddSessionPayload( - new UserError("The title cannot be empty.", "TITLE_EMPTY")); - } - - if (input.SpeakerIds.Count == 0) - { - return new AddSessionPayload( - new UserError("No speaker assigned.", "NO_SPEAKER")); - } - - var session = new Session - { - Title = input.Title, - Abstract = input.Abstract, - }; - - foreach (int speakerId in input.SpeakerIds) - { - session.SessionSpeakers.Add(new SessionSpeaker - { - SpeakerId = speakerId - }); - } - - context.Sessions.Add(session); - await context.SaveChangesAsync(cancellationToken); - - return new AddSessionPayload(session); - } - - [UseApplicationDbContext] - public async Task ScheduleSessionAsync( - ScheduleSessionInput input, - [ScopedService] ApplicationDbContext context, - [Service]ITopicEventSender eventSender) - { - if (input.EndTime < input.StartTime) - { - return new ScheduleSessionPayload( - new UserError("endTime has to be larger than startTime.", "END_TIME_INVALID")); - } - - var session = await context.Sessions.FindAsync(input.SessionId); - - if (session is null) - { - return new ScheduleSessionPayload( - new UserError("Session not found.", "SESSION_NOT_FOUND")); - } - - session.TrackId = input.TrackId; - session.StartTime = input.StartTime; - session.EndTime = input.EndTime; - - await context.SaveChangesAsync(); - - await eventSender.SendAsync( - nameof(SessionSubscriptions.OnSessionScheduledAsync), - session.Id); - - return new ScheduleSessionPayload(session); - } - - [UseApplicationDbContext] - public async Task RenameSessionAsync( - RenameSessionInput input, - [ScopedService] ApplicationDbContext context, - [Service]ITopicEventSender eventSender) - { - var session = await context.Sessions.FindAsync(input.SessionId); - - if (session is null) - { - return new RenameSessionPayload( - new UserError("Session not found.", "SESSION_NOT_FOUND")); - } - - session.Title = input.Title; - - await context.SaveChangesAsync(); - - await eventSender.SendAsync( - nameof(SessionSubscriptions.OnSessionScheduledAsync), - session.Id); - - return new RenameSessionPayload(session); - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Sessions/SessionNode.cs b/code/complete/GraphQL/Sessions/SessionNode.cs deleted file mode 100644 index 934885f..0000000 --- a/code/complete/GraphQL/Sessions/SessionNode.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; -using Microsoft.EntityFrameworkCore; - -namespace ConferencePlanner.GraphQL.Sessions -{ - [Node] - [ExtendObjectType(typeof(Session))] - public class SessionNode - { - [BindMember(nameof(Session.SessionSpeakers), Replace = true)] - public Task GetSpeakersAsync( - [Parent] Session session, - SpeakerBySessionIdDataLoader speakerBySessionId, - CancellationToken cancellationToken) - => speakerBySessionId.LoadAsync(session.Id, cancellationToken); - - [UsePaging(ConnectionName = "SessionAttendees")] - [BindMember(nameof(Session.SessionAttendees), Replace = true)] - public IQueryable GetAttendees( - [Parent] Session session, - [ScopedService] ApplicationDbContext dbContext) - => dbContext.Sessions - .Where(s => s.Id == session.Id) - .Include(s => s.SessionAttendees) - .SelectMany(s => s.SessionAttendees.Select(t => t.Attendee!)); - - public async Task GetTrackAsync( - [Parent] Session session, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) - => session.TrackId is not null - ? await trackById.LoadAsync(session.TrackId.Value, cancellationToken) - : null; - - [NodeResolver] - public static Task GetSessionByIdAsync( - int id, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - => sessionById.LoadAsync(id, cancellationToken); - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Sessions/SessionPayloadBase.cs b/code/complete/GraphQL/Sessions/SessionPayloadBase.cs deleted file mode 100644 index 888ad50..0000000 --- a/code/complete/GraphQL/Sessions/SessionPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class SessionPayloadBase : Payload - { - protected SessionPayloadBase(Session session) - { - Session = session; - } - - protected SessionPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Session? Session { get; } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Sessions/SessionQueries.cs b/code/complete/GraphQL/Sessions/SessionQueries.cs deleted file mode 100644 index 8540ecc..0000000 --- a/code/complete/GraphQL/Sessions/SessionQueries.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Data; -using HotChocolate.Types; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Sessions -{ - [ExtendObjectType(OperationTypeNames.Query)] - public class SessionQueries - { - [UseApplicationDbContext] - [UsePaging] - [UseFiltering(typeof(SessionFilterInputType))] - [UseSorting] - public IQueryable GetSessions( - [ScopedService] ApplicationDbContext context) - => context.Sessions; - - public Task GetSessionByIdAsync( - [ID(nameof(Session))] int id, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - => sessionById.LoadAsync(id, cancellationToken); - - public async Task> GetSessionsByIdAsync( - [ID(nameof(Session))] int[] ids, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - => await sessionById.LoadAsync(ids, cancellationToken); - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Sessions/SessionSubscriptions.cs b/code/complete/GraphQL/Sessions/SessionSubscriptions.cs deleted file mode 100644 index cd05bbd..0000000 --- a/code/complete/GraphQL/Sessions/SessionSubscriptions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Sessions -{ - [ExtendObjectType(OperationTypeNames.Subscription)] - public class SessionSubscriptions - { - [Subscribe] - [Topic] - public Task OnSessionScheduledAsync( - [EventMessage] int sessionId, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - sessionById.LoadAsync(sessionId, cancellationToken); - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Speakers/AddSpeakerInput.cs b/code/complete/GraphQL/Speakers/AddSpeakerInput.cs deleted file mode 100644 index a81f45f..0000000 --- a/code/complete/GraphQL/Speakers/AddSpeakerInput.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ConferencePlanner.GraphQL.Speakers -{ - public record AddSpeakerInput( - string Name, - string? Bio, - string? WebSite); -} \ No newline at end of file diff --git a/code/complete/GraphQL/Speakers/AddSpeakerPayload.cs b/code/complete/GraphQL/Speakers/AddSpeakerPayload.cs deleted file mode 100644 index aaf0ab0..0000000 --- a/code/complete/GraphQL/Speakers/AddSpeakerPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Speakers -{ - public class AddSpeakerPayload : SpeakerPayloadBase - { - public AddSpeakerPayload(Speaker speaker) - : base(speaker) - { - } - - public AddSpeakerPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Speakers/ModifySpeakerInput.cs b/code/complete/GraphQL/Speakers/ModifySpeakerInput.cs deleted file mode 100644 index 65b727e..0000000 --- a/code/complete/GraphQL/Speakers/ModifySpeakerInput.cs +++ /dev/null @@ -1,13 +0,0 @@ -using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Speakers -{ - public record ModifySpeakerInput( - [property: ID(nameof(Speaker))] - int Id, - Optional Name, - Optional Bio, - Optional WebSite); -} \ No newline at end of file diff --git a/code/complete/GraphQL/Speakers/ModifySpeakerPayload.cs b/code/complete/GraphQL/Speakers/ModifySpeakerPayload.cs deleted file mode 100644 index c692356..0000000 --- a/code/complete/GraphQL/Speakers/ModifySpeakerPayload.cs +++ /dev/null @@ -1,18 +0,0 @@ -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Speakers -{ - public class ModifySpeakerPayload : SpeakerPayloadBase - { - public ModifySpeakerPayload(Speaker speaker) - : base(speaker) - { - } - - public ModifySpeakerPayload(UserError error) - : base(new [] { error }) - { - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Speakers/SpeakerMutations.cs b/code/complete/GraphQL/Speakers/SpeakerMutations.cs deleted file mode 100644 index 798f001..0000000 --- a/code/complete/GraphQL/Speakers/SpeakerMutations.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Speakers -{ - [ExtendObjectType(OperationTypeNames.Mutation)] - public class SpeakerMutations - { - [UseApplicationDbContext] - public async Task AddSpeakerAsync( - AddSpeakerInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - var speaker = new Speaker - { - Name = input.Name, - Bio = input.Bio, - WebSite = input.WebSite - }; - - context.Speakers.Add(speaker); - await context.SaveChangesAsync(cancellationToken); - - return new AddSpeakerPayload(speaker); - } - - [UseApplicationDbContext] - public async Task ModifySpeakerAsync( - ModifySpeakerInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - if (input.Name.HasValue && input.Name.Value is null) - { - return new ModifySpeakerPayload( - new UserError("Name cannot be null", "NAME_NULL")); - } - - Speaker? speaker = await context.Speakers.FindAsync(input.Id); - - if (speaker is null) - { - return new ModifySpeakerPayload( - new UserError("Speaker with id not found.", "SPEAKER_NOT_FOUND")); - } - - if (input.Name.HasValue) - { - speaker.Name = input.Name; - } - - if (input.Bio.HasValue) - { - speaker.Bio = input.Bio; - } - - if (input.WebSite.HasValue) - { - speaker.WebSite = input.WebSite; - } - - await context.SaveChangesAsync(cancellationToken); - - return new ModifySpeakerPayload(speaker); - } - - [UseApplicationDbContext] - public async Task UploadSpeakerPhotoAsync( - UploadSpeakerPhotoInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - Speaker? speaker = await context.Speakers.FindAsync(input.Id); - - if (speaker is null) - { - return new UploadSpeakerPhotoPayload( - new UserError("Speaker with id not found.", "SPEAKER_NOT_FOUND")); - } - -/* - if (input.Photo.Length < 1024_0000) - { - using (Stream inputStream = input.Photo.OpenReadStream()) - { - using(Stream outputStream = File.) - - await File.WriteAllBytesAsync(speaker.Id + ".png", ); - } - } -*/ - - return new UploadSpeakerPhotoPayload(speaker); - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Speakers/SpeakerNode.cs b/code/complete/GraphQL/Speakers/SpeakerNode.cs deleted file mode 100644 index b452ae8..0000000 --- a/code/complete/GraphQL/Speakers/SpeakerNode.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; -using Microsoft.EntityFrameworkCore; - -namespace ConferencePlanner.GraphQL.Speakers -{ - [Node] - [ExtendObjectType(typeof(Speaker))] - public class SpeakerNode - { - [BindMember(nameof(Speaker.Bio), Replace = true)] - public string? GetBio([Parent] Speaker speaker, bool error = false) - { - if(error) - { - throw new GraphQLException("Some error with the bio."); - } - - return speaker.Bio; - } - - [BindMember(nameof(Speaker.SessionSpeakers), Replace = true)] - public async Task> GetSessionsAsync( - [Parent] Speaker speaker, - SessionBySpeakerIdDataLoader sessionBySpeakerId, - CancellationToken cancellationToken) - => await sessionBySpeakerId.LoadAsync(speaker.Id, cancellationToken); - - [NodeResolver] - public static Task GetSpeakerByIdAsync( - int id, - SpeakerByIdDataLoader speakerById, - CancellationToken cancellationToken) - => speakerById.LoadAsync(id, cancellationToken); - - public async Task> GetSessionsExpensiveAsync( - [Parent] Speaker speaker, - SessionBySpeakerIdDataLoader sessionBySpeakerId, - CancellationToken cancellationToken) - { - await Task.Delay(new Random().Next(1000, 3000), cancellationToken); - - return await sessionBySpeakerId.LoadAsync(speaker.Id, cancellationToken); - } - - public async IAsyncEnumerable GetSessionsStreamAsync( - [Parent] Speaker speaker, - [Service] IDbContextFactory contextFactory, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - var random = new Random(); - - await Task.Delay(random.Next(500, 1000), cancellationToken); - - await using var context = contextFactory.CreateDbContext(); - - var stream = (IAsyncEnumerable)context.Speakers - .Where(s => s.Id == speaker.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers) - .Include(s => s.Session); - - await foreach (var item in stream.WithCancellation(cancellationToken)) - { - if (item.Session is not null) - { - yield return item.Session; - } - - await Task.Delay(random.Next(100, 300), cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Speakers/SpeakerPayloadBase.cs b/code/complete/GraphQL/Speakers/SpeakerPayloadBase.cs deleted file mode 100644 index 0a7801d..0000000 --- a/code/complete/GraphQL/Speakers/SpeakerPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Speakers -{ - public class SpeakerPayloadBase : Payload - { - protected SpeakerPayloadBase(Speaker speaker) - { - Speaker = speaker; - } - - protected SpeakerPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Speaker? Speaker { get; } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Speakers/SpeakerQueries.cs b/code/complete/GraphQL/Speakers/SpeakerQueries.cs deleted file mode 100644 index ca26b0b..0000000 --- a/code/complete/GraphQL/Speakers/SpeakerQueries.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Speakers -{ - [ExtendObjectType(OperationTypeNames.Query)] - public class SpeakerQueries - { - [UseApplicationDbContext] - [UsePaging] - public IQueryable GetSpeakers( - [ScopedService] ApplicationDbContext context) - => context.Speakers.OrderBy(t => t.Name); - - public Task GetSpeakerByIdAsync( - [ID(nameof(Speaker))] int id, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) - => dataLoader.LoadAsync(id, cancellationToken); - - public async Task> GetSpeakersByIdAsync( - [ID(nameof(Speaker))] int[] ids, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) - => await dataLoader.LoadAsync(ids, cancellationToken); - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Speakers/UploadSpeakerPhotoInput.cs b/code/complete/GraphQL/Speakers/UploadSpeakerPhotoInput.cs deleted file mode 100644 index f6421da..0000000 --- a/code/complete/GraphQL/Speakers/UploadSpeakerPhotoInput.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Speakers -{ - public record UploadSpeakerPhotoInput([ID(nameof(Speaker))]int Id, IFile Photo); -} \ No newline at end of file diff --git a/code/complete/GraphQL/Speakers/UploadSpeakerPhotoPayload.cs b/code/complete/GraphQL/Speakers/UploadSpeakerPhotoPayload.cs deleted file mode 100644 index f2d834b..0000000 --- a/code/complete/GraphQL/Speakers/UploadSpeakerPhotoPayload.cs +++ /dev/null @@ -1,20 +0,0 @@ -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Speakers -{ - public class UploadSpeakerPhotoPayload : SpeakerPayloadBase - { - public UploadSpeakerPhotoPayload(Speaker speaker) - : base(speaker) - { - } - - public UploadSpeakerPhotoPayload(UserError error) - : base(new[] { error }) - { - } - - - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Startup.cs b/code/complete/GraphQL/Startup.cs deleted file mode 100644 index dd362df..0000000 --- a/code/complete/GraphQL/Startup.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using ConferencePlanner.GraphQL.Attendees; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using ConferencePlanner.GraphQL.Imports; -using ConferencePlanner.GraphQL.Sessions; -using ConferencePlanner.GraphQL.Speakers; -using ConferencePlanner.GraphQL.Tracks; -using GreenDonut; -using HotChocolate; -using HotChocolate.AspNetCore; -using HotChocolate.Execution; -using HotChocolate.Execution.Instrumentation; -using HotChocolate.Resolvers; -using HotChocolate.Types; -using HotChocolate.Types.Pagination; - -namespace ConferencePlanner.GraphQL -{ - 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 - .AddCors(o => - o.AddDefaultPolicy(b => - b.AllowAnyHeader() - .AllowAnyMethod() - .AllowAnyOrigin())) - - // First we add the DBContext which we will be using to interact with our - // Database. - .AddPooledDbContextFactory( - (s, o) => o - .UseSqlite("Data Source=conferences.db") - .UseLoggerFactory(s.GetRequiredService())) - - // This adds the GraphQL server core service and declares a schema. - .AddGraphQLServer() - - // Next we add the types to our schema. - .AddQueryType() - .AddMutationType() - .AddSubscriptionType() - - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddDataLoader() - - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddDataLoader() - .AddDataLoader() - - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddDataLoader() - .AddDataLoader() - - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddDataLoader() - - .AddType() - - // In this section we are adding extensions like relay helpers, - // filtering and sorting. - .AddFiltering() - .AddSorting() - .AddGlobalObjectIdentification() - - // we make sure that the db exists and prefill it with conference data. - .EnsureDatabaseIsCreated() - - // Since we are using subscriptions, we need to register a pub/sub system. - // for our demo we are using a in-memory pub/sub system. - .AddInMemorySubscriptions() - - // Last we add support for persisted queries. - // The first line adds the persisted query storage, - // the second one the persisted query processing pipeline. - .AddFileSystemQueryStorage("./persisted_queries") - .UsePersistedQueryPipeline(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseCors(); - - app.UseWebSockets(); - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - // We will be using the new routing API to host our GraphQL middleware. - endpoints.MapGraphQL() - .WithOptions(new GraphQLServerOptions - { - Tool = - { - GaTrackingId = "G-2Y04SFDV8F" - } - }); - - endpoints.MapGet("/", context => - { - context.Response.Redirect("/graphql", true); - return Task.CompletedTask; - }); - }); - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Tracks/AddTrackInput.cs b/code/complete/GraphQL/Tracks/AddTrackInput.cs deleted file mode 100644 index 5c83b34..0000000 --- a/code/complete/GraphQL/Tracks/AddTrackInput.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace ConferencePlanner.GraphQL.Tracks -{ - public record AddTrackInput(string Name); -} \ No newline at end of file diff --git a/code/complete/GraphQL/Tracks/AddTrackPayload.cs b/code/complete/GraphQL/Tracks/AddTrackPayload.cs deleted file mode 100644 index 8f35b13..0000000 --- a/code/complete/GraphQL/Tracks/AddTrackPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class AddTrackPayload : TrackPayloadBase - { - public AddTrackPayload(Track track) - : base(track) - { - } - - public AddTrackPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Tracks/RenameTrackInput.cs b/code/complete/GraphQL/Tracks/RenameTrackInput.cs deleted file mode 100644 index bfec3ea..0000000 --- a/code/complete/GraphQL/Tracks/RenameTrackInput.cs +++ /dev/null @@ -1,9 +0,0 @@ -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public record RenameTrackInput( - [property: ID(nameof(Track))] int Id, - string Name); -} \ No newline at end of file diff --git a/code/complete/GraphQL/Tracks/RenameTrackPayload.cs b/code/complete/GraphQL/Tracks/RenameTrackPayload.cs deleted file mode 100644 index ca4c8a1..0000000 --- a/code/complete/GraphQL/Tracks/RenameTrackPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class RenameTrackPayload : TrackPayloadBase - { - public RenameTrackPayload(Track track) - : base(track) - { - } - - public RenameTrackPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Tracks/TrackMutations.cs b/code/complete/GraphQL/Tracks/TrackMutations.cs deleted file mode 100644 index d1a0df7..0000000 --- a/code/complete/GraphQL/Tracks/TrackMutations.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Tracks -{ - [ExtendObjectType(OperationTypeNames.Mutation)] - public class TrackMutations - { - [UseApplicationDbContext] - public async Task AddTrackAsync( - AddTrackInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - var track = new Track { Name = input.Name }; - context.Tracks.Add(track); - - await context.SaveChangesAsync(cancellationToken); - - return new AddTrackPayload(track); - } - - [UseApplicationDbContext] - public async Task RenameTrackAsync( - RenameTrackInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - var track = await context.Tracks.FindAsync(input.Id, cancellationToken); - - if (track is null) - { - throw new GraphQLException("Track not found."); - } - - track.Name = input.Name; - - await context.SaveChangesAsync(cancellationToken); - - return new RenameTrackPayload(track); - } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Tracks/TrackNode.cs b/code/complete/GraphQL/Tracks/TrackNode.cs deleted file mode 100644 index 5792578..0000000 --- a/code/complete/GraphQL/Tracks/TrackNode.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Tracks -{ - [Node] - [ExtendObjectType(typeof(Track))] - public class TrackNode - { - [UseUpperCase] - public string GetName([Parent] Track track) => track.Name!; - - [UseApplicationDbContext] - [UsePaging(ConnectionName = "TrackSessions")] - public IQueryable GetSessions( - [Parent] Track track, - [ScopedService] ApplicationDbContext dbContext) - => dbContext.Tracks.Where(t => t.Id == track.Id).SelectMany(t => t.Sessions); - - [NodeResolver] - public static Task GetTrackByIdAsync( - int id, - TrackByIdDataLoader trackByIdDataLoader, - CancellationToken cancellationToken) - => trackByIdDataLoader.LoadAsync(id, cancellationToken); - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Tracks/TrackPayloadBase.cs b/code/complete/GraphQL/Tracks/TrackPayloadBase.cs deleted file mode 100644 index de11da7..0000000 --- a/code/complete/GraphQL/Tracks/TrackPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class TrackPayloadBase : Payload - { - public TrackPayloadBase(Track track) - { - Track = track; - } - - public TrackPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Track? Track { get; } - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/Tracks/TrackQueries.cs b/code/complete/GraphQL/Tracks/TrackQueries.cs deleted file mode 100644 index 95cbe26..0000000 --- a/code/complete/GraphQL/Tracks/TrackQueries.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Tracks -{ - [ExtendObjectType(OperationTypeNames.Query)] - public class TrackQueries - { - [UseApplicationDbContext] - [UsePaging] - public IQueryable GetTracks( - [ScopedService] ApplicationDbContext context) - => context.Tracks.OrderBy(t => t.Name); - - [UseApplicationDbContext] - public Task GetTrackByNameAsync( - string name, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - => context.Tracks.FirstAsync(t => t.Name == name, cancellationToken); - - [UseApplicationDbContext] - public async Task> GetTrackByNamesAsync( - string[] names, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - => await context.Tracks.Where(t => names.Contains(t.Name)).ToListAsync(cancellationToken); - - public Task GetTrackByIdAsync( - [ID(nameof(Track))] int id, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) - => trackById.LoadAsync(id, cancellationToken); - - public async Task> GetSessionsByIdAsync( - [ID(nameof(Track))] int[] ids, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) - => await trackById.LoadAsync(ids, cancellationToken); - } -} \ No newline at end of file diff --git a/code/complete/GraphQL/appsettings.Development.json b/code/complete/GraphQL/appsettings.Development.json deleted file mode 100644 index 7679dfb..0000000 --- a/code/complete/GraphQL/appsettings.Development.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information", - "Microsoft.EntityFrameworkCore.Database.Command": "Information" - } - } -} diff --git a/code/complete/GraphQL/appsettings.json b/code/complete/GraphQL/appsettings.json deleted file mode 100644 index 3662be1..0000000 --- a/code/complete/GraphQL/appsettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information", - "Microsoft.EntityFrameworkCore.Database.Command": "Information" - } - }, - "AllowedHosts": "*" -} diff --git a/code/complete/GraphQL/dockerfile b/code/complete/GraphQL/dockerfile deleted file mode 100644 index 9e2d821..0000000 --- a/code/complete/GraphQL/dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -# syntax=docker/dockerfile:1 -FROM mcr.microsoft.com/dotnet/sdk:6.0.100-alpine3.14-amd64 AS build-env -WORKDIR /app - -# Copy csproj and restore as distinct layers -COPY ./ ./ -RUN dotnet restore -RUN dotnet publish -c Release -o out - -# Build runtime image -FROM mcr.microsoft.com/dotnet/aspnet:6.0.0-alpine3.14-amd64 -WORKDIR /app -COPY --from=build-env /app/out . -ENTRYPOINT ["dotnet", "GraphQL.dll"] \ No newline at end of file diff --git a/code/complete/GraphQL/persisted_queries/get-sessions.graphql b/code/complete/GraphQL/persisted_queries/get-sessions.graphql deleted file mode 100644 index cacdb61..0000000 --- a/code/complete/GraphQL/persisted_queries/get-sessions.graphql +++ /dev/null @@ -1,18 +0,0 @@ -query GetSessions( - $first: PaginationAmount! = 10, - $after: String) { - sessions( - first: $first - after: $after - where: { startTime_not: null } - order_by: { title: ASC }) { - nodes { - title - startTime - endTime - } - pageInfo { - endCursor - } - } -} \ No newline at end of file diff --git a/code/complete/global.json b/code/complete/global.json deleted file mode 100644 index 572a2a6..0000000 --- a/code/complete/global.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "sdk": { - "version": "6.0.200" - } -} diff --git a/code/complete/schema.graphql b/code/complete/schema.graphql deleted file mode 100644 index 7a1e0e3..0000000 --- a/code/complete/schema.graphql +++ /dev/null @@ -1,465 +0,0 @@ -schema { - query: Query - mutation: Mutation - subscription: Subscription -} - -"The node interface is implemented by entities that have a global unique identifier." -interface Node { - id: ID! -} - -type AddSessionPayload { - session: Session - errors: [UserError!] -} - -type AddSpeakerPayload { - speaker: Speaker - errors: [UserError!] -} - -type AddTrackPayload { - track: Track - errors: [UserError!] -} - -type Attendee implements Node { - id: ID! - sessions: [Session] - firstName: String! - lastName: String! - userName: String! - emailAddress: String -} - -"A connection to a list of items." -type AttendeeConnection { - "Information to aid in pagination." - pageInfo: PageInfo! - "A list of edges." - edges: [AttendeeEdge!] - "A flattened list of the nodes." - nodes: [Attendee!] -} - -"An edge in a connection." -type AttendeeEdge { - "A cursor for use in pagination." - cursor: String! - "The item at the end of the edge." - node: Attendee! -} - -type CheckInAttendeePayload { - session: Session - attendee: Attendee - errors: [UserError!] -} - -type ModifySpeakerPayload { - speaker: Speaker - errors: [UserError!] -} - -type Mutation { - registerAttendee(input: RegisterAttendeeInput!): RegisterAttendeePayload! - checkInAttendee(input: CheckInAttendeeInput!): CheckInAttendeePayload! - addSession(input: AddSessionInput!): AddSessionPayload! - scheduleSession(input: ScheduleSessionInput!): ScheduleSessionPayload! - addSpeaker(input: AddSpeakerInput!): AddSpeakerPayload! - modifySpeaker(input: ModifySpeakerInput!): ModifySpeakerPayload! - addTrack(input: AddTrackInput!): AddTrackPayload! - renameTrack(input: RenameTrackInput!): RenameTrackPayload! -} - -"Information about pagination in a connection." -type PageInfo { - "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 - "When paginating forwards, the cursor to continue." - endCursor: String -} - -type Query { - node(id: ID!): Node - attendees(first: Int after: String last: Int before: String): AttendeeConnection - attendeeById(id: ID!): Attendee! - attendeesById(ids: [ID!]!): [Attendee!]! - sessions(first: Int after: String last: Int before: String where: SessionFilterInput order: [SessionSortInput]): SessionConnection - sessionById(id: ID!): Session! - sessionsById(ids: [ID!]!): [Session!]! - speakers(first: Int after: String last: Int before: String): SpeakerConnection - speakerById(id: ID!): Speaker! - speakersById(ids: [ID!]!): [Speaker!]! - tracks(first: Int after: String last: Int before: String): TrackConnection - trackByName(name: String!): Track! - trackByNames(names: [String!]!): [Track!]! - trackById(id: ID!): Track! -} - -type RegisterAttendeePayload { - attendee: Attendee - errors: [UserError!] -} - -type RenameTrackPayload { - track: Track - errors: [UserError!] -} - -type ScheduleSessionPayload { - track: Track - speakers: [Speaker!] - session: Session - errors: [UserError!] -} - -type Session implements Node { - id: ID! - speakers: [Speaker] - attendees: [Attendee] - track: Track - trackId: ID - title: String! - abstract: String - startTime: DateTime - endTime: DateTime - duration: TimeSpan! -} - -type SessionAttendeeCheckIn { - checkInCount: Int! - attendee: Attendee! - attendeeId: ID! - sessionId: ID! -} - -"A connection to a list of items." -type SessionConnection { - "Information to aid in pagination." - pageInfo: PageInfo! - "A list of edges." - edges: [SessionEdge!] - "A flattened list of the nodes." - nodes: [Session!] -} - -"An edge in a connection." -type SessionEdge { - "A cursor for use in pagination." - cursor: String! - "The item at the end of the edge." - node: Session! -} - -type Speaker implements Node { - id: ID! - sessions: [Session] - name: String! - bio: String - webSite: String -} - -"A connection to a list of items." -type SpeakerConnection { - "Information to aid in pagination." - pageInfo: PageInfo! - "A list of edges." - edges: [SpeakerEdge!] - "A flattened list of the nodes." - nodes: [Speaker!] -} - -"An edge in a connection." -type SpeakerEdge { - "A cursor for use in pagination." - cursor: String! - "The item at the end of the edge." - node: Speaker! -} - -type Subscription { - onAttendeeCheckedIn(sessionId: ID!): SessionAttendeeCheckIn! - onSessionScheduled: Session! -} - -type Track implements Node { - id: ID! - sessions(first: Int after: String last: Int before: String): SessionConnection - name: String! -} - -"A connection to a list of items." -type TrackConnection { - "Information to aid in pagination." - pageInfo: PageInfo! - "A list of edges." - edges: [TrackEdge!] - "A flattened list of the nodes." - nodes: [Track!] -} - -"An edge in a connection." -type TrackEdge { - "A cursor for use in pagination." - cursor: String! - "The item at the end of the edge." - node: Track! -} - -type UserError { - message: String! - code: String! -} - -input AddSessionInput { - title: String! - abstract: String - speakerIds: [ID!]! -} - -input AddSpeakerInput { - name: String! - bio: String - webSite: String -} - -input AddTrackInput { - name: String! -} - -input AttendeeFilterInput { - and: [AttendeeFilterInput!] - or: [AttendeeFilterInput!] - id: ComparableOperationFilterInputOfInt32FilterInput - firstName: StringOperationFilterInput - lastName: StringOperationFilterInput - userName: StringOperationFilterInput - emailAddress: StringOperationFilterInput - sessionsAttendees: ListFilterInputOfFilterInputTypeOfSessionAttendeeFilterInput -} - -input CheckInAttendeeInput { - sessionId: ID! - attendeeId: ID! -} - -input ComparableOperationFilterInputOfInt32FilterInput { - eq: Int - neq: Int - in: [Int!] - nin: [Int!] - gt: Int - ngt: Int - gte: Int - ngte: Int - lt: Int - nlt: Int - lte: Int - nlte: Int -} - -input ComparableOperationFilterInputOfNullableOfDateTimeOffsetFilterInput { - eq: DateTime - neq: DateTime - in: [DateTime] - nin: [DateTime] - gt: DateTime - ngt: DateTime - gte: DateTime - ngte: DateTime - lt: DateTime - nlt: DateTime - lte: DateTime - nlte: DateTime -} - -input ComparableOperationFilterInputOfNullableOfInt32FilterInput { - eq: Int - neq: Int - in: [Int] - nin: [Int] - gt: Int - ngt: Int - gte: Int - ngte: Int - lt: Int - nlt: Int - lte: Int - nlte: Int -} - -input ComparableOperationFilterInputOfTimeSpanFilterInput { - eq: TimeSpan - neq: TimeSpan - in: [TimeSpan!] - nin: [TimeSpan!] - gt: TimeSpan - ngt: TimeSpan - gte: TimeSpan - ngte: TimeSpan - lt: TimeSpan - nlt: TimeSpan - lte: TimeSpan - nlte: TimeSpan -} - -input ListFilterInputOfFilterInputTypeOfSessionAttendeeFilterInput { - all: SessionAttendeeFilterInput - none: SessionAttendeeFilterInput - some: SessionAttendeeFilterInput - any: Boolean -} - -input ListFilterInputOfFilterInputTypeOfSessionFilterInput { - all: SessionFilterInput - none: SessionFilterInput - some: SessionFilterInput - any: Boolean -} - -input ListFilterInputOfFilterInputTypeOfSessionSpeakerFilterInput { - all: SessionSpeakerFilterInput - none: SessionSpeakerFilterInput - some: SessionSpeakerFilterInput - any: Boolean -} - -input ModifySpeakerInput { - id: ID! - name: String - bio: String - webSite: String -} - -input RegisterAttendeeInput { - firstName: String! - lastName: String! - userName: String! - emailAddress: String! -} - -input RenameTrackInput { - id: ID! - name: String! -} - -input ScheduleSessionInput { - sessionId: ID! - trackId: ID! - startTime: DateTime! - endTime: DateTime! -} - -input SessionAttendeeFilterInput { - and: [SessionAttendeeFilterInput!] - or: [SessionAttendeeFilterInput!] - sessionId: ComparableOperationFilterInputOfInt32FilterInput - session: SessionFilterInput - attendeeId: ComparableOperationFilterInputOfInt32FilterInput - attendee: AttendeeFilterInput -} - -input SessionFilterInput { - and: [SessionFilterInput!] - or: [SessionFilterInput!] - id: ComparableOperationFilterInputOfInt32FilterInput - title: StringOperationFilterInput - abstract: StringOperationFilterInput - startTime: ComparableOperationFilterInputOfNullableOfDateTimeOffsetFilterInput - endTime: ComparableOperationFilterInputOfNullableOfDateTimeOffsetFilterInput - duration: ComparableOperationFilterInputOfTimeSpanFilterInput - trackId: ComparableOperationFilterInputOfNullableOfInt32FilterInput - sessionSpeakers: ListFilterInputOfFilterInputTypeOfSessionSpeakerFilterInput - sessionAttendees: ListFilterInputOfFilterInputTypeOfSessionAttendeeFilterInput - track: TrackFilterInput -} - -input SessionSortInput { - id: DefaultSortEnumType - title: DefaultSortEnumType - abstract: DefaultSortEnumType - startTime: DefaultSortEnumType - endTime: DefaultSortEnumType - duration: DefaultSortEnumType - trackId: DefaultSortEnumType - track: TrackSortInput -} - -input SessionSpeakerFilterInput { - and: [SessionSpeakerFilterInput!] - or: [SessionSpeakerFilterInput!] - sessionId: ComparableOperationFilterInputOfInt32FilterInput - session: SessionFilterInput - speakerId: ComparableOperationFilterInputOfInt32FilterInput - speaker: SpeakerFilterInput -} - -input SpeakerFilterInput { - and: [SpeakerFilterInput!] - or: [SpeakerFilterInput!] - id: ComparableOperationFilterInputOfInt32FilterInput - name: StringOperationFilterInput - bio: StringOperationFilterInput - webSite: StringOperationFilterInput - sessionSpeakers: ListFilterInputOfFilterInputTypeOfSessionSpeakerFilterInput -} - -input StringOperationFilterInput { - and: [StringOperationFilterInput!] - or: [StringOperationFilterInput!] - eq: String - neq: String - contains: String - ncontains: String - in: [String] - nin: [String] - startsWith: String - nstartsWith: String - endsWith: String - nendsWith: String -} - -input TrackFilterInput { - and: [TrackFilterInput!] - or: [TrackFilterInput!] - id: ComparableOperationFilterInputOfInt32FilterInput - name: StringOperationFilterInput - sessions: ListFilterInputOfFilterInputTypeOfSessionFilterInput -} - -input TrackSortInput { - id: DefaultSortEnumType - name: DefaultSortEnumType -} - -enum DefaultSortEnumType { - ASC - DESC -} - -"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`." -directive @defer("If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String "Deferred when true." if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT - -"The @deprecated directive is used within the type system definition language to indicate deprecated portions of a GraphQL service’s schema,such as deprecated fields on a type or deprecated enum values." -directive @deprecated("Deprecations include a reason for why it is deprecated, which is formatted using Markdown syntax (as specified by CommonMark)." reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE - -"Directs the executor to include this field or fragment only when the `if` argument is true." -directive @include("Included when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - -"Directs the executor to skip this field or fragment when the `if` argument is true." -directive @skip("Skipped when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT - -"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." -directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! "Streamed when true." if: Boolean!) on FIELD - -"The `DateTime` scalar represents an ISO-8601 compliant date time type." -scalar DateTime - -"The `TimeSpan` scalar represents an ISO-8601 compliant duration type." -scalar TimeSpan \ No newline at end of file diff --git a/code/global.json b/code/global.json index 572a2a6..e972eb1 100644 --- a/code/global.json +++ b/code/global.json @@ -1,5 +1,6 @@ { "sdk": { - "version": "6.0.200" + "version": "8.0.300", + "rollForward": "latestFeature" } } diff --git a/code/session-1/.config/dotnet-tools.json b/code/session-1/.config/dotnet-tools.json index c735fef..efc07e5 100644 --- a/code/session-1/.config/dotnet-tools.json +++ b/code/session-1/.config/dotnet-tools.json @@ -1,12 +1,13 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "dotnet-ef": { - "version": "5.0.0", - "commands": [ - "dotnet-ef" - ] - } - } +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "9.0.1", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } } \ No newline at end of file diff --git a/code/session-1/.vscode/tasks.json b/code/session-1/.vscode/tasks.json deleted file mode 100644 index 31c32bd..0000000 --- a/code/session-1/.vscode/tasks.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - // 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/code/session-1/ConferencePlanner.sln b/code/session-1/ConferencePlanner.sln index 42fadb8..8c414fc 100644 --- a/code/session-1/ConferencePlanner.sln +++ b/code/session-1/ConferencePlanner.sln @@ -1,34 +1,22 @@ - -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}") = "GraphQL", "GraphQL\GraphQL.csproj", "{48385280-56F1-4937-9655-E6A79184740B}" -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 - {48385280-56F1-4937-9655-E6A79184740B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x64.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x64.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x86.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x86.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|Any CPU.Build.0 = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x64.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x64.Build.0 = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x86.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL", "GraphQL\GraphQL.csproj", "{D96823B9-86D3-4D54-A803-F1D43AEBE1FD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/code/session-1/GraphQL/AddSpeakerInput.cs b/code/session-1/GraphQL/AddSpeakerInput.cs index 75e63d4..0cdd569 100644 --- a/code/session-1/GraphQL/AddSpeakerInput.cs +++ b/code/session-1/GraphQL/AddSpeakerInput.cs @@ -1,7 +1,6 @@ -namespace ConferencePlanner.GraphQL.Speakers -{ - public record AddSpeakerInput( - string Name, - string Bio, - string WebSite); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL; + +public sealed record AddSpeakerInput( + string Name, + string? Bio, + string? Website); diff --git a/code/session-1/GraphQL/AddSpeakerPayload.cs b/code/session-1/GraphQL/AddSpeakerPayload.cs index 101c76b..d81e139 100644 --- a/code/session-1/GraphQL/AddSpeakerPayload.cs +++ b/code/session-1/GraphQL/AddSpeakerPayload.cs @@ -1,14 +1,8 @@ using ConferencePlanner.GraphQL.Data; -namespace ConferencePlanner.GraphQL -{ - public class AddSpeakerPayload - { - public AddSpeakerPayload(Speaker speaker) - { - Speaker = speaker; - } +namespace ConferencePlanner.GraphQL; - public Speaker Speaker { get; } - } -} \ No newline at end of file +public sealed class AddSpeakerPayload(Speaker speaker) +{ + public Speaker Speaker { get; } = speaker; +} diff --git a/code/session-1/GraphQL/Data/ApplicationDbContext.cs b/code/session-1/GraphQL/Data/ApplicationDbContext.cs index e3bb42c..3d40505 100644 --- a/code/session-1/GraphQL/Data/ApplicationDbContext.cs +++ b/code/session-1/GraphQL/Data/ApplicationDbContext.cs @@ -1,14 +1,9 @@ using Microsoft.EntityFrameworkCore; - namespace ConferencePlanner.GraphQL.Data - { - public class ApplicationDbContext : DbContext - { - public ApplicationDbContext(DbContextOptions options) - : base(options) - { - } +namespace ConferencePlanner.GraphQL.Data; - public DbSet Speakers { get; set; } - } - } \ No newline at end of file +public sealed class ApplicationDbContext(DbContextOptions options) + : DbContext(options) +{ + public DbSet Speakers { get; init; } +} diff --git a/code/session-1/GraphQL/Data/Speaker.cs b/code/session-1/GraphQL/Data/Speaker.cs index 40de4fa..837893b 100644 --- a/code/session-1/GraphQL/Data/Speaker.cs +++ b/code/session-1/GraphQL/Data/Speaker.cs @@ -1,19 +1,17 @@ using System.ComponentModel.DataAnnotations; - namespace ConferencePlanner.GraphQL.Data - { - public class Speaker - { - public int Id { get; set; } +namespace ConferencePlanner.GraphQL.Data; - [Required] - [StringLength(200)] - public string Name { get; set; } +public sealed class Speaker +{ + public int Id { get; init; } - [StringLength(4000)] - public string Bio { get; set; } + [StringLength(200)] + public required string Name { get; init; } - [StringLength(1000)] - public virtual string WebSite { get; set; } - } - } \ No newline at end of file + [StringLength(4000)] + public string? Bio { get; init; } + + [StringLength(1000)] + public string? Website { get; init; } +} diff --git a/code/session-1/GraphQL/GraphQL.csproj b/code/session-1/GraphQL/GraphQL.csproj index 9f219f5..97d215a 100644 --- a/code/session-1/GraphQL/GraphQL.csproj +++ b/code/session-1/GraphQL/GraphQL.csproj @@ -1,17 +1,22 @@ - - - - net5.0 - ConferencePlanner.GraphQL - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - + + + + net8.0 + enable + enable + ConferencePlanner.GraphQL + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/code/session-1/GraphQL/Migrations/20201010183502_Initial.cs b/code/session-1/GraphQL/Migrations/20201010183502_Initial.cs deleted file mode 100644 index 69e30fb..0000000 --- a/code/session-1/GraphQL/Migrations/20201010183502_Initial.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace GraphQL.Migrations -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Speakers", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), - Bio = table.Column(type: "TEXT", maxLength: 4000, nullable: true), - WebSite = table.Column(type: "TEXT", maxLength: 1000, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Speakers", x => x.Id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Speakers"); - } - } -} diff --git a/code/session-4/GraphQL/Migrations/20201010183502_Initial.Designer.cs b/code/session-1/GraphQL/Migrations/20240807140835_Initial.Designer.cs similarity index 57% rename from code/session-4/GraphQL/Migrations/20201010183502_Initial.Designer.cs rename to code/session-1/GraphQL/Migrations/20240807140835_Initial.Designer.cs index dc170c1..d508064 100644 --- a/code/session-4/GraphQL/Migrations/20201010183502_Initial.Designer.cs +++ b/code/session-1/GraphQL/Migrations/20240807140835_Initial.Designer.cs @@ -4,37 +4,46 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20201010183502_Initial")] + [Migration("20240807140835_Initial")] partial class Initial { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Bio") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("WebSite") + b.Property("Website") .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(1000)"); b.HasKey("Id"); diff --git a/code/complete/GraphQL/Migrations/20200725133424_Initial.cs b/code/session-1/GraphQL/Migrations/20240807140835_Initial.cs similarity index 50% rename from code/complete/GraphQL/Migrations/20200725133424_Initial.cs rename to code/session-1/GraphQL/Migrations/20240807140835_Initial.cs index eaf1c1c..7301c7a 100644 --- a/code/complete/GraphQL/Migrations/20200725133424_Initial.cs +++ b/code/session-1/GraphQL/Migrations/20240807140835_Initial.cs @@ -1,20 +1,25 @@ using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable namespace ConferencePlanner.GraphQL.Migrations { + /// public partial class Initial : Migration { + /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "Speakers", columns: table => new { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(maxLength: 200, nullable: false), - Bio = table.Column(maxLength: 4000, nullable: true), - WebSite = table.Column(maxLength: 1000, nullable: true) + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Bio = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + Website = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true) }, constraints: table => { @@ -22,6 +27,7 @@ protected override void Up(MigrationBuilder migrationBuilder) }); } + /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( diff --git a/code/session-1/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs b/code/session-1/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs index a2b20f6..c386e45 100644 --- a/code/session-1/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/code/session-1/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs @@ -3,8 +3,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { [DbContext(typeof(ApplicationDbContext))] partial class ApplicationDbContextModelSnapshot : ModelSnapshot @@ -13,26 +16,31 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Bio") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("WebSite") + b.Property("Website") .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(1000)"); b.HasKey("Id"); diff --git a/code/session-1/GraphQL/Mutation.cs b/code/session-1/GraphQL/Mutation.cs deleted file mode 100644 index ddf1842..0000000 --- a/code/session-1/GraphQL/Mutation.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.Speakers; -using HotChocolate; - -namespace ConferencePlanner.GraphQL -{ - public class Mutation - { - public async Task AddSpeakerAsync( - AddSpeakerInput input, - [Service] ApplicationDbContext context) - { - var speaker = new Speaker - { - Name = input.Name, - Bio = input.Bio, - WebSite = input.WebSite - }; - - context.Speakers.Add(speaker); - await context.SaveChangesAsync(); - - return new AddSpeakerPayload(speaker); - } - } -} \ No newline at end of file diff --git a/code/session-1/GraphQL/Mutations.cs b/code/session-1/GraphQL/Mutations.cs new file mode 100644 index 0000000..823f107 --- /dev/null +++ b/code/session-1/GraphQL/Mutations.cs @@ -0,0 +1,26 @@ +using ConferencePlanner.GraphQL.Data; + +namespace ConferencePlanner.GraphQL; + +public static class Mutations +{ + [Mutation] + public static async Task AddSpeakerAsync( + AddSpeakerInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var speaker = new Speaker + { + Name = input.Name, + Bio = input.Bio, + Website = input.Website + }; + + dbContext.Speakers.Add(speaker); + + await dbContext.SaveChangesAsync(cancellationToken); + + return new AddSpeakerPayload(speaker); + } +} diff --git a/code/session-1/GraphQL/Program.cs b/code/session-1/GraphQL/Program.cs index c4914c6..99984a7 100644 --- a/code/session-1/GraphQL/Program.cs +++ b/code/session-1/GraphQL/Program.cs @@ -1,26 +1,16 @@ -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 ConferencePlanner.GraphQL -{ - 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(); - }); - } -} +using ConferencePlanner.GraphQL.Data; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddDbContext( + options => options.UseNpgsql("Host=127.0.0.1;Username=graphql_workshop;Password=secret")) + .AddGraphQLServer() + .AddGraphQLTypes(); + +var app = builder.Build(); + +app.MapGraphQL(); + +await app.RunWithGraphQLCommandsAsync(args); diff --git a/code/session-1/GraphQL/Properties/launchSettings.json b/code/session-1/GraphQL/Properties/launchSettings.json new file mode 100644 index 0000000..c0d9484 --- /dev/null +++ b/code/session-1/GraphQL/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7000;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/code/session-1/GraphQL/Queries.cs b/code/session-1/GraphQL/Queries.cs new file mode 100644 index 0000000..6c38c8f --- /dev/null +++ b/code/session-1/GraphQL/Queries.cs @@ -0,0 +1,15 @@ +using ConferencePlanner.GraphQL.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL; + +public static class Queries +{ + [Query] + public static async Task> GetSpeakersAsync( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + return await dbContext.Speakers.AsNoTracking().ToListAsync(cancellationToken); + } +} diff --git a/code/session-1/GraphQL/Query.cs b/code/session-1/GraphQL/Query.cs deleted file mode 100644 index fc2464d..0000000 --- a/code/session-1/GraphQL/Query.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Linq; -using HotChocolate; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL -{ - public class Query - { - public IQueryable GetSpeakers([Service] ApplicationDbContext context) => - context.Speakers; - } -} \ No newline at end of file diff --git a/code/session-1/GraphQL/Startup.cs b/code/session-1/GraphQL/Startup.cs deleted file mode 100644 index 1bea098..0000000 --- a/code/session-1/GraphQL/Startup.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL; -using ConferencePlanner.GraphQL.Data; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace ConferencePlanner.GraphQL -{ - 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.AddDbContext( - options => options.UseSqlite("Data Source=conferences.db")); - - services - .AddGraphQLServer() - .AddQueryType() - .AddMutationType(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGraphQL(); - }); - } - } -} diff --git a/code/session-1/GraphQL/appsettings.Development.json b/code/session-1/GraphQL/appsettings.Development.json index dba68eb..0c208ae 100644 --- a/code/session-1/GraphQL/appsettings.Development.json +++ b/code/session-1/GraphQL/appsettings.Development.json @@ -1,9 +1,8 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Information", + "Microsoft.AspNetCore": "Warning" } } } diff --git a/code/session-1/GraphQL/appsettings.json b/code/session-1/GraphQL/appsettings.json index 81ff877..10f68b8 100644 --- a/code/session-1/GraphQL/appsettings.json +++ b/code/session-1/GraphQL/appsettings.json @@ -1,10 +1,9 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/code/session-1/docker-compose.yml b/code/session-1/docker-compose.yml new file mode 100644 index 0000000..728458d --- /dev/null +++ b/code/session-1/docker-compose.yml @@ -0,0 +1,23 @@ +name: graphql-workshop + +services: + graphql-workshop-postgres: + container_name: graphql-workshop-postgres + image: postgres:17.2 + environment: + POSTGRES_USER: graphql_workshop + POSTGRES_PASSWORD: secret + POSTGRES_DB: graphql_workshop + networks: [graphql-workshop] + ports: ["5432:5432"] + volumes: + - type: volume + source: postgres-data + target: /var/lib/postgresql/data + +networks: + graphql-workshop: + name: graphql-workshop + +volumes: + postgres-data: diff --git a/code/session-2/.config/dotnet-tools.json b/code/session-2/.config/dotnet-tools.json index c735fef..aad8137 100644 --- a/code/session-2/.config/dotnet-tools.json +++ b/code/session-2/.config/dotnet-tools.json @@ -3,10 +3,11 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "5.0.0", + "version": "9.0.1", "commands": [ "dotnet-ef" - ] + ], + "rollForward": false } } -} \ No newline at end of file +} diff --git a/code/session-2/.vscode/tasks.json b/code/session-2/.vscode/tasks.json deleted file mode 100644 index 31c32bd..0000000 --- a/code/session-2/.vscode/tasks.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - // 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/code/session-2/ConferencePlanner.sln b/code/session-2/ConferencePlanner.sln index 42fadb8..8c414fc 100644 --- a/code/session-2/ConferencePlanner.sln +++ b/code/session-2/ConferencePlanner.sln @@ -1,34 +1,22 @@ - -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}") = "GraphQL", "GraphQL\GraphQL.csproj", "{48385280-56F1-4937-9655-E6A79184740B}" -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 - {48385280-56F1-4937-9655-E6A79184740B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x64.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x64.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x86.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x86.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|Any CPU.Build.0 = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x64.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x64.Build.0 = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x86.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL", "GraphQL\GraphQL.csproj", "{D96823B9-86D3-4D54-A803-F1D43AEBE1FD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/code/session-2/GraphQL/AddSpeakerInput.cs b/code/session-2/GraphQL/AddSpeakerInput.cs index 51b929b..0cdd569 100644 --- a/code/session-2/GraphQL/AddSpeakerInput.cs +++ b/code/session-2/GraphQL/AddSpeakerInput.cs @@ -1,7 +1,6 @@ -namespace ConferencePlanner.GraphQL -{ - public record AddSpeakerInput( - string Name, - string? Bio, - string? WebSite); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL; + +public sealed record AddSpeakerInput( + string Name, + string? Bio, + string? Website); diff --git a/code/session-2/GraphQL/AddSpeakerPayload.cs b/code/session-2/GraphQL/AddSpeakerPayload.cs index 101c76b..d81e139 100644 --- a/code/session-2/GraphQL/AddSpeakerPayload.cs +++ b/code/session-2/GraphQL/AddSpeakerPayload.cs @@ -1,14 +1,8 @@ using ConferencePlanner.GraphQL.Data; -namespace ConferencePlanner.GraphQL -{ - public class AddSpeakerPayload - { - public AddSpeakerPayload(Speaker speaker) - { - Speaker = speaker; - } +namespace ConferencePlanner.GraphQL; - public Speaker Speaker { get; } - } -} \ No newline at end of file +public sealed class AddSpeakerPayload(Speaker speaker) +{ + public Speaker Speaker { get; } = speaker; +} diff --git a/code/session-2/GraphQL/Data/ApplicationDbContext.cs b/code/session-2/GraphQL/Data/ApplicationDbContext.cs index 46ae366..5a2d633 100644 --- a/code/session-2/GraphQL/Data/ApplicationDbContext.cs +++ b/code/session-2/GraphQL/Data/ApplicationDbContext.cs @@ -1,14 +1,33 @@ using Microsoft.EntityFrameworkCore; - namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class ApplicationDbContext(DbContextOptions options) + : DbContext(options) +{ + public DbSet Attendees { get; init; } + + public DbSet Sessions { get; init; } + + public DbSet Speakers { get; init; } + + public DbSet Tracks { get; init; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) { - public class ApplicationDbContext : DbContext - { - public ApplicationDbContext(DbContextOptions options) - : base(options) - { - } - - public DbSet Speakers { get; set; } = default!; - } - } \ No newline at end of file + modelBuilder + .Entity() + .HasIndex(a => a.Username) + .IsUnique(); + + // Many-to-many: Session <-> Attendee + modelBuilder + .Entity() + .HasKey(sa => new { sa.SessionId, sa.AttendeeId }); + + // Many-to-many: Speaker <-> Session + modelBuilder + .Entity() + .HasKey(ss => new { ss.SessionId, ss.SpeakerId }); + } +} diff --git a/code/session-2/GraphQL/Data/Attendee.cs b/code/session-2/GraphQL/Data/Attendee.cs new file mode 100644 index 0000000..304ec00 --- /dev/null +++ b/code/session-2/GraphQL/Data/Attendee.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Attendee +{ + public int Id { get; init; } + + [StringLength(200)] + public required string FirstName { get; init; } + + [StringLength(200)] + public required string LastName { get; init; } + + [StringLength(200)] + public required string Username { get; init; } + + [StringLength(256)] + public string? EmailAddress { get; init; } + + public ICollection SessionsAttendees { get; init; } = + new List(); +} diff --git a/code/session-2/GraphQL/Data/Session.cs b/code/session-2/GraphQL/Data/Session.cs new file mode 100644 index 0000000..2793ec4 --- /dev/null +++ b/code/session-2/GraphQL/Data/Session.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; + +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Session +{ + public int Id { get; init; } + + [StringLength(200)] + public required string Title { get; init; } + + [StringLength(4000)] + public string? Abstract { get; init; } + + public DateTimeOffset? StartTime { get; init; } + + public DateTimeOffset? EndTime { get; init; } + + // Bonus points to those who can figure out why this is written this way. + public TimeSpan Duration => + EndTime?.Subtract(StartTime ?? EndTime ?? DateTimeOffset.MinValue) ?? + TimeSpan.Zero; + + public int? TrackId { get; init; } + + public ICollection SessionSpeakers { get; init; } = + new List(); + + public ICollection SessionAttendees { get; init; } = + new List(); + + public Track? Track { get; init; } +} diff --git a/code/session-2/GraphQL/Data/SessionAttendee.cs b/code/session-2/GraphQL/Data/SessionAttendee.cs new file mode 100644 index 0000000..892d5ae --- /dev/null +++ b/code/session-2/GraphQL/Data/SessionAttendee.cs @@ -0,0 +1,12 @@ +namespace ConferencePlanner.GraphQL.Data; + +public sealed class SessionAttendee +{ + public int SessionId { get; init; } + + public Session Session { get; init; } = null!; + + public int AttendeeId { get; init; } + + public Attendee Attendee { get; init; } = null!; +} diff --git a/code/session-2/GraphQL/Data/SessionSpeaker.cs b/code/session-2/GraphQL/Data/SessionSpeaker.cs new file mode 100644 index 0000000..aeebe62 --- /dev/null +++ b/code/session-2/GraphQL/Data/SessionSpeaker.cs @@ -0,0 +1,12 @@ +namespace ConferencePlanner.GraphQL.Data; + +public sealed class SessionSpeaker +{ + public int SessionId { get; init; } + + public Session Session { get; init; } = null!; + + public int SpeakerId { get; init; } + + public Speaker Speaker { get; init; } = null!; +} diff --git a/code/session-2/GraphQL/Data/Speaker.cs b/code/session-2/GraphQL/Data/Speaker.cs index afe29dd..bf47876 100644 --- a/code/session-2/GraphQL/Data/Speaker.cs +++ b/code/session-2/GraphQL/Data/Speaker.cs @@ -1,19 +1,20 @@ using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Speaker { - public class Speaker - { - public int Id { get; set; } + public int Id { get; init; } + + [StringLength(200)] + public required string Name { get; init; } - [Required] - [StringLength(200)] - public string? Name { get; set; } + [StringLength(4000)] + public string? Bio { get; init; } - [StringLength(4000)] - public string? Bio { get; set; } + [StringLength(1000)] + public string? Website { get; init; } - [StringLength(1000)] - public virtual string? WebSite { get; set; } - } -} \ No newline at end of file + public ICollection SessionSpeakers { get; init; } = + new List(); +} diff --git a/code/session-2/GraphQL/Data/Track.cs b/code/session-2/GraphQL/Data/Track.cs new file mode 100644 index 0000000..3d87993 --- /dev/null +++ b/code/session-2/GraphQL/Data/Track.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Track +{ + public int Id { get; init; } + + [StringLength(200)] + public required string Name { get; init; } + + public ICollection Sessions { get; init; } = + new List(); +} diff --git a/code/session-2/GraphQL/DataLoaders.cs b/code/session-2/GraphQL/DataLoaders.cs new file mode 100644 index 0000000..bedd7fd --- /dev/null +++ b/code/session-2/GraphQL/DataLoaders.cs @@ -0,0 +1,36 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL; + +public static class DataLoaders +{ + [DataLoader] + public static async Task> SpeakerByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Speakers + .AsNoTracking() + .Where(s => ids.Contains(s.Id)) + .Select(s => s.Id, selector) + .ToDictionaryAsync(s => s.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SessionsBySpeakerIdAsync( + IReadOnlyList speakerIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Speakers + .AsNoTracking() + .Where(s => speakerIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionSpeakers.Select(ss => ss.Session), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-2/GraphQL/GraphQL.csproj b/code/session-2/GraphQL/GraphQL.csproj index a717e60..97d215a 100644 --- a/code/session-2/GraphQL/GraphQL.csproj +++ b/code/session-2/GraphQL/GraphQL.csproj @@ -1,18 +1,22 @@ - - - - net5.0 - ConferencePlanner.GraphQL - enable - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - + + + + net8.0 + enable + enable + ConferencePlanner.GraphQL + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/code/session-2/GraphQL/Migrations/20201010183502_Initial.cs b/code/session-2/GraphQL/Migrations/20201010183502_Initial.cs deleted file mode 100644 index 69e30fb..0000000 --- a/code/session-2/GraphQL/Migrations/20201010183502_Initial.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace GraphQL.Migrations -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Speakers", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), - Bio = table.Column(type: "TEXT", maxLength: 4000, nullable: true), - WebSite = table.Column(type: "TEXT", maxLength: 1000, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Speakers", x => x.Id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Speakers"); - } - } -} diff --git a/code/session-2/GraphQL/Migrations/20201010183502_Initial.Designer.cs b/code/session-2/GraphQL/Migrations/20240807140835_Initial.Designer.cs similarity index 57% rename from code/session-2/GraphQL/Migrations/20201010183502_Initial.Designer.cs rename to code/session-2/GraphQL/Migrations/20240807140835_Initial.Designer.cs index dc170c1..d508064 100644 --- a/code/session-2/GraphQL/Migrations/20201010183502_Initial.Designer.cs +++ b/code/session-2/GraphQL/Migrations/20240807140835_Initial.Designer.cs @@ -4,37 +4,46 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20201010183502_Initial")] + [Migration("20240807140835_Initial")] partial class Initial { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Bio") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("WebSite") + b.Property("Website") .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(1000)"); b.HasKey("Id"); diff --git a/code/session-2/GraphQL/Migrations/20240807140835_Initial.cs b/code/session-2/GraphQL/Migrations/20240807140835_Initial.cs new file mode 100644 index 0000000..7301c7a --- /dev/null +++ b/code/session-2/GraphQL/Migrations/20240807140835_Initial.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Speakers", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Bio = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + Website = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Speakers", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Speakers"); + } + } +} diff --git a/code/session-3/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs b/code/session-2/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs similarity index 74% rename from code/session-3/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs rename to code/session-2/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs index 2e3d723..75c788f 100644 --- a/code/session-3/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs +++ b/code/session-2/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs @@ -5,47 +5,56 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20201010202211_Refactoring")] + [Migration("20240812080119_Refactoring")] partial class Refactoring { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("EmailAddress") .HasMaxLength(256) - .HasColumnType("TEXT"); + .HasColumnType("character varying(256)"); b.Property("FirstName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("LastName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("UserName") + b.Property("Username") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); - b.HasIndex("UserName") + b.HasIndex("Username") .IsUnique(); b.ToTable("Attendees"); @@ -55,25 +64,27 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Abstract") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("EndTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("StartTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("Title") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("TrackId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("Id"); @@ -85,10 +96,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("AttendeeId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "AttendeeId"); @@ -100,10 +111,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("SpeakerId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "SpeakerId"); @@ -116,20 +127,22 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Bio") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("WebSite") + b.Property("Website") .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(1000)"); b.HasKey("Id"); @@ -140,12 +153,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); diff --git a/code/session-3/GraphQL/Migrations/20201010202211_Refactoring.cs b/code/session-2/GraphQL/Migrations/20240812080119_Refactoring.cs similarity index 68% rename from code/session-3/GraphQL/Migrations/20201010202211_Refactoring.cs rename to code/session-2/GraphQL/Migrations/20240812080119_Refactoring.cs index ffdcfeb..e544f2e 100644 --- a/code/session-3/GraphQL/Migrations/20201010202211_Refactoring.cs +++ b/code/session-2/GraphQL/Migrations/20240812080119_Refactoring.cs @@ -1,22 +1,27 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { + /// public partial class Refactoring : Migration { + /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "Attendees", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - FirstName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - LastName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - UserName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - EmailAddress = table.Column(type: "TEXT", maxLength: 256, nullable: true) + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + FirstName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + LastName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Username = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + EmailAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true) }, constraints: table => { @@ -27,9 +32,9 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Tracks", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", maxLength: 200, nullable: false) + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false) }, constraints: table => { @@ -40,13 +45,13 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Sessions", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Title = table.Column(type: "TEXT", maxLength: 200, nullable: false), - Abstract = table.Column(type: "TEXT", maxLength: 4000, nullable: true), - StartTime = table.Column(type: "TEXT", nullable: true), - EndTime = table.Column(type: "TEXT", nullable: true), - TrackId = table.Column(type: "INTEGER", nullable: true) + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Abstract = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + StartTime = table.Column(type: "timestamp with time zone", nullable: true), + EndTime = table.Column(type: "timestamp with time zone", nullable: true), + TrackId = table.Column(type: "integer", nullable: true) }, constraints: table => { @@ -55,16 +60,15 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "FK_Sessions_Tracks_TrackId", column: x => x.TrackId, principalTable: "Tracks", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); + principalColumn: "Id"); }); migrationBuilder.CreateTable( name: "SessionAttendee", columns: table => new { - SessionId = table.Column(type: "INTEGER", nullable: false), - AttendeeId = table.Column(type: "INTEGER", nullable: false) + SessionId = table.Column(type: "integer", nullable: false), + AttendeeId = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -87,8 +91,8 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "SessionSpeaker", columns: table => new { - SessionId = table.Column(type: "INTEGER", nullable: false), - SpeakerId = table.Column(type: "INTEGER", nullable: false) + SessionId = table.Column(type: "integer", nullable: false), + SpeakerId = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -108,9 +112,9 @@ protected override void Up(MigrationBuilder migrationBuilder) }); migrationBuilder.CreateIndex( - name: "IX_Attendees_UserName", + name: "IX_Attendees_Username", table: "Attendees", - column: "UserName", + column: "Username", unique: true); migrationBuilder.CreateIndex( @@ -129,6 +133,7 @@ protected override void Up(MigrationBuilder migrationBuilder) column: "SpeakerId"); } + /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( diff --git a/code/session-2/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs b/code/session-2/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs index a2b20f6..6cc21a5 100644 --- a/code/session-2/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/code/session-2/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1,10 +1,14 @@ // +using System; using ConferencePlanner.GraphQL.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { [DbContext(typeof(ApplicationDbContext))] partial class ApplicationDbContextModelSnapshot : ModelSnapshot @@ -13,31 +17,221 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EmailAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Attendees"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Abstract") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TrackId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.ToTable("Sessions"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => + { + b.Property("SessionId") + .HasColumnType("integer"); + + b.Property("AttendeeId") + .HasColumnType("integer"); + + b.HasKey("SessionId", "AttendeeId"); + + b.HasIndex("AttendeeId"); + + b.ToTable("SessionAttendee"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => + { + b.Property("SessionId") + .HasColumnType("integer"); + + b.Property("SpeakerId") + .HasColumnType("integer"); + + b.HasKey("SessionId", "SpeakerId"); + + b.HasIndex("SpeakerId"); + + b.ToTable("SessionSpeaker"); + }); modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Bio") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("WebSite") + b.Property("Website") .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(1000)"); b.HasKey("Id"); b.ToTable("Speakers"); }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => + { + b.HasOne("ConferencePlanner.GraphQL.Data.Track", "Track") + .WithMany("Sessions") + .HasForeignKey("TrackId"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => + { + b.HasOne("ConferencePlanner.GraphQL.Data.Attendee", "Attendee") + .WithMany("SessionsAttendees") + .HasForeignKey("AttendeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ConferencePlanner.GraphQL.Data.Session", "Session") + .WithMany("SessionAttendees") + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Attendee"); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => + { + b.HasOne("ConferencePlanner.GraphQL.Data.Session", "Session") + .WithMany("SessionSpeakers") + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ConferencePlanner.GraphQL.Data.Speaker", "Speaker") + .WithMany("SessionSpeakers") + .HasForeignKey("SpeakerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Session"); + + b.Navigation("Speaker"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => + { + b.Navigation("SessionsAttendees"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => + { + b.Navigation("SessionAttendees"); + + b.Navigation("SessionSpeakers"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => + { + b.Navigation("SessionSpeakers"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Track", b => + { + b.Navigation("Sessions"); + }); #pragma warning restore 612, 618 } } diff --git a/code/session-2/GraphQL/Mutation.cs b/code/session-2/GraphQL/Mutation.cs deleted file mode 100644 index 7ac1d09..0000000 --- a/code/session-2/GraphQL/Mutation.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using HotChocolate; - -namespace ConferencePlanner.GraphQL -{ - public class Mutation - { - public async Task AddSpeakerAsync( - AddSpeakerInput input, - [Service] ApplicationDbContext context) - { - var speaker = new Speaker - { - Name = input.Name, - Bio = input.Bio, - WebSite = input.WebSite - }; - - context.Speakers.Add(speaker); - await context.SaveChangesAsync(); - - return new AddSpeakerPayload(speaker); - } - } -} \ No newline at end of file diff --git a/code/session-2/GraphQL/Mutations.cs b/code/session-2/GraphQL/Mutations.cs new file mode 100644 index 0000000..823f107 --- /dev/null +++ b/code/session-2/GraphQL/Mutations.cs @@ -0,0 +1,26 @@ +using ConferencePlanner.GraphQL.Data; + +namespace ConferencePlanner.GraphQL; + +public static class Mutations +{ + [Mutation] + public static async Task AddSpeakerAsync( + AddSpeakerInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var speaker = new Speaker + { + Name = input.Name, + Bio = input.Bio, + Website = input.Website + }; + + dbContext.Speakers.Add(speaker); + + await dbContext.SaveChangesAsync(cancellationToken); + + return new AddSpeakerPayload(speaker); + } +} diff --git a/code/session-2/GraphQL/Program.cs b/code/session-2/GraphQL/Program.cs index c4914c6..99984a7 100644 --- a/code/session-2/GraphQL/Program.cs +++ b/code/session-2/GraphQL/Program.cs @@ -1,26 +1,16 @@ -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 ConferencePlanner.GraphQL -{ - 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(); - }); - } -} +using ConferencePlanner.GraphQL.Data; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddDbContext( + options => options.UseNpgsql("Host=127.0.0.1;Username=graphql_workshop;Password=secret")) + .AddGraphQLServer() + .AddGraphQLTypes(); + +var app = builder.Build(); + +app.MapGraphQL(); + +await app.RunWithGraphQLCommandsAsync(args); diff --git a/code/session-2/GraphQL/Properties/launchSettings.json b/code/session-2/GraphQL/Properties/launchSettings.json new file mode 100644 index 0000000..c0d9484 --- /dev/null +++ b/code/session-2/GraphQL/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7000;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/code/session-2/GraphQL/Queries.cs b/code/session-2/GraphQL/Queries.cs new file mode 100644 index 0000000..d05dee4 --- /dev/null +++ b/code/session-2/GraphQL/Queries.cs @@ -0,0 +1,27 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL; + +public static class Queries +{ + [Query] + public static async Task> GetSpeakersAsync( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + return await dbContext.Speakers.AsNoTracking().ToListAsync(cancellationToken); + } + + [Query] + public static async Task GetSpeakerAsync( + int id, + ISpeakerByIdDataLoader speakerById, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakerById.Select(selection).LoadAsync(id, cancellationToken); + } +} diff --git a/code/session-2/GraphQL/Query.cs b/code/session-2/GraphQL/Query.cs deleted file mode 100644 index fc2464d..0000000 --- a/code/session-2/GraphQL/Query.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Linq; -using HotChocolate; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL -{ - public class Query - { - public IQueryable GetSpeakers([Service] ApplicationDbContext context) => - context.Speakers; - } -} \ No newline at end of file diff --git a/code/session-2/GraphQL/Startup.cs b/code/session-2/GraphQL/Startup.cs deleted file mode 100644 index 1bea098..0000000 --- a/code/session-2/GraphQL/Startup.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL; -using ConferencePlanner.GraphQL.Data; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace ConferencePlanner.GraphQL -{ - 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.AddDbContext( - options => options.UseSqlite("Data Source=conferences.db")); - - services - .AddGraphQLServer() - .AddQueryType() - .AddMutationType(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGraphQL(); - }); - } - } -} diff --git a/code/session-2/GraphQL/Types/SessionType.cs b/code/session-2/GraphQL/Types/SessionType.cs new file mode 100644 index 0000000..4f28617 --- /dev/null +++ b/code/session-2/GraphQL/Types/SessionType.cs @@ -0,0 +1,10 @@ +using ConferencePlanner.GraphQL.Data; + +namespace ConferencePlanner.GraphQL.Types; + +[ObjectType] +public static partial class SessionType +{ + public static TimeSpan Duration([Parent("StartTime EndTime")] Session session) + => session.Duration; +} diff --git a/code/session-2/GraphQL/Types/SpeakerType.cs b/code/session-2/GraphQL/Types/SpeakerType.cs new file mode 100644 index 0000000..892cf8c --- /dev/null +++ b/code/session-2/GraphQL/Types/SpeakerType.cs @@ -0,0 +1,21 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Types; + +[ObjectType] +public static partial class SpeakerType +{ + [BindMember(nameof(Speaker.SessionSpeakers))] + public static async Task> GetSessionsAsync( + [Parent] Speaker speaker, + ISessionsBySpeakerIdDataLoader sessionsBySpeakerId, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsBySpeakerId + .Select(selection) + .LoadRequiredAsync(speaker.Id, cancellationToken); + } +} diff --git a/code/session-2/GraphQL/appsettings.Development.json b/code/session-2/GraphQL/appsettings.Development.json index dba68eb..0c208ae 100644 --- a/code/session-2/GraphQL/appsettings.Development.json +++ b/code/session-2/GraphQL/appsettings.Development.json @@ -1,9 +1,8 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Information", + "Microsoft.AspNetCore": "Warning" } } } diff --git a/code/session-2/GraphQL/appsettings.json b/code/session-2/GraphQL/appsettings.json index 81ff877..10f68b8 100644 --- a/code/session-2/GraphQL/appsettings.json +++ b/code/session-2/GraphQL/appsettings.json @@ -1,10 +1,9 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/code/session-2/docker-compose.yml b/code/session-2/docker-compose.yml new file mode 100644 index 0000000..728458d --- /dev/null +++ b/code/session-2/docker-compose.yml @@ -0,0 +1,23 @@ +name: graphql-workshop + +services: + graphql-workshop-postgres: + container_name: graphql-workshop-postgres + image: postgres:17.2 + environment: + POSTGRES_USER: graphql_workshop + POSTGRES_PASSWORD: secret + POSTGRES_DB: graphql_workshop + networks: [graphql-workshop] + ports: ["5432:5432"] + volumes: + - type: volume + source: postgres-data + target: /var/lib/postgresql/data + +networks: + graphql-workshop: + name: graphql-workshop + +volumes: + postgres-data: diff --git a/code/session-3/.config/dotnet-tools.json b/code/session-3/.config/dotnet-tools.json index c735fef..aad8137 100644 --- a/code/session-3/.config/dotnet-tools.json +++ b/code/session-3/.config/dotnet-tools.json @@ -3,10 +3,11 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "5.0.0", + "version": "9.0.1", "commands": [ "dotnet-ef" - ] + ], + "rollForward": false } } -} \ No newline at end of file +} diff --git a/code/session-3/.vscode/launch.json b/code/session-3/.vscode/launch.json deleted file mode 100644 index e90375e..0000000 --- a/code/session-3/.vscode/launch.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - // 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}/GraphQL/bin/Debug/net5.0/GraphQL.dll", - "args": [], - "cwd": "${workspaceFolder}/GraphQL", - "stopAtEntry": false, - "serverReadyAction": { - "action": "openExternally", - "pattern": "\\bNow 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/code/session-3/.vscode/tasks.json b/code/session-3/.vscode/tasks.json deleted file mode 100644 index 31c32bd..0000000 --- a/code/session-3/.vscode/tasks.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - // 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/code/session-3/ConferencePlanner.sln b/code/session-3/ConferencePlanner.sln index 42fadb8..8c414fc 100644 --- a/code/session-3/ConferencePlanner.sln +++ b/code/session-3/ConferencePlanner.sln @@ -1,34 +1,22 @@ - -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}") = "GraphQL", "GraphQL\GraphQL.csproj", "{48385280-56F1-4937-9655-E6A79184740B}" -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 - {48385280-56F1-4937-9655-E6A79184740B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x64.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x64.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x86.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x86.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|Any CPU.Build.0 = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x64.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x64.Build.0 = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x86.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL", "GraphQL\GraphQL.csproj", "{D96823B9-86D3-4D54-A803-F1D43AEBE1FD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/code/session-3/GraphQL/AddSpeakerInput.cs b/code/session-3/GraphQL/AddSpeakerInput.cs deleted file mode 100644 index 51b929b..0000000 --- a/code/session-3/GraphQL/AddSpeakerInput.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ConferencePlanner.GraphQL -{ - public record AddSpeakerInput( - string Name, - string? Bio, - string? WebSite); -} \ No newline at end of file diff --git a/code/session-3/GraphQL/AddSpeakerPayload.cs b/code/session-3/GraphQL/AddSpeakerPayload.cs deleted file mode 100644 index 101c76b..0000000 --- a/code/session-3/GraphQL/AddSpeakerPayload.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL -{ - public class AddSpeakerPayload - { - public AddSpeakerPayload(Speaker speaker) - { - Speaker = speaker; - } - - public Speaker Speaker { get; } - } -} \ No newline at end of file diff --git a/code/session-3/GraphQL/Attendees/AttendeeDataLoaders.cs b/code/session-3/GraphQL/Attendees/AttendeeDataLoaders.cs new file mode 100644 index 0000000..56b8e21 --- /dev/null +++ b/code/session-3/GraphQL/Attendees/AttendeeDataLoaders.cs @@ -0,0 +1,36 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Attendees; + +public static class AttendeeDataLoaders +{ + [DataLoader] + public static async Task> AttendeeByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Attendees + .AsNoTracking() + .Where(a => ids.Contains(a.Id)) + .Select(a => a.Id, selector) + .ToDictionaryAsync(a => a.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SessionsByAttendeeIdAsync( + IReadOnlyList attendeeIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Attendees + .AsNoTracking() + .Where(a => attendeeIds.Contains(a.Id)) + .Select(a => a.Id, a => a.SessionsAttendees.Select(sa => sa.Session), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-3/GraphQL/Attendees/AttendeeType.cs b/code/session-3/GraphQL/Attendees/AttendeeType.cs new file mode 100644 index 0000000..a76e76a --- /dev/null +++ b/code/session-3/GraphQL/Attendees/AttendeeType.cs @@ -0,0 +1,32 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Attendees; + +[ObjectType] +public static partial class AttendeeType +{ + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .ImplementsNode() + .IdField(a => a.Id) + .ResolveNode( + async (ctx, id) + => await ctx.DataLoader() + .LoadAsync(id, ctx.RequestAborted)); + } + + [BindMember(nameof(Attendee.SessionsAttendees))] + public static async Task> GetSessionsAsync( + [Parent] Attendee attendee, + ISessionsByAttendeeIdDataLoader sessionsByAttendeeId, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsByAttendeeId + .Select(selection) + .LoadRequiredAsync(attendee.Id, cancellationToken); + } +} diff --git a/code/session-3/GraphQL/Data/ApplicationDbContext.cs b/code/session-3/GraphQL/Data/ApplicationDbContext.cs index bbd7dda..5a2d633 100644 --- a/code/session-3/GraphQL/Data/ApplicationDbContext.cs +++ b/code/session-3/GraphQL/Data/ApplicationDbContext.cs @@ -1,38 +1,33 @@ - using Microsoft.EntityFrameworkCore; - - namespace ConferencePlanner.GraphQL.Data - { - public class ApplicationDbContext : DbContext - { - public ApplicationDbContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasIndex(a => a.UserName) - .IsUnique(); - - // Many-to-many: Session <-> Attendee - modelBuilder - .Entity() - .HasKey(ca => new { ca.SessionId, ca.AttendeeId }); - - // Many-to-many: Speaker <-> Session - modelBuilder - .Entity() - .HasKey(ss => new { ss.SessionId, ss.SpeakerId }); - } - - public DbSet Sessions { get; set; } = default!; - - public DbSet Tracks { get; set; } = default!; - - public DbSet Speakers { get; set; } = default!; - - public DbSet Attendees { get; set; } = default!; - } - } \ No newline at end of file +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Data; + +public sealed class ApplicationDbContext(DbContextOptions options) + : DbContext(options) +{ + public DbSet Attendees { get; init; } + + public DbSet Sessions { get; init; } + + public DbSet Speakers { get; init; } + + public DbSet Tracks { get; init; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .HasIndex(a => a.Username) + .IsUnique(); + + // Many-to-many: Session <-> Attendee + modelBuilder + .Entity() + .HasKey(sa => new { sa.SessionId, sa.AttendeeId }); + + // Many-to-many: Speaker <-> Session + modelBuilder + .Entity() + .HasKey(ss => new { ss.SessionId, ss.SpeakerId }); + } +} diff --git a/code/session-3/GraphQL/Data/Attendee.cs b/code/session-3/GraphQL/Data/Attendee.cs index e3f9ab0..304ec00 100644 --- a/code/session-3/GraphQL/Data/Attendee.cs +++ b/code/session-3/GraphQL/Data/Attendee.cs @@ -1,28 +1,23 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Attendee { - public class Attendee - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? FirstName { get; set; } + [StringLength(200)] + public required string FirstName { get; init; } - [Required] - [StringLength(200)] - public string? LastName { get; set; } + [StringLength(200)] + public required string LastName { get; init; } - [Required] - [StringLength(200)] - public string? UserName { get; set; } + [StringLength(200)] + public required string Username { get; init; } - [StringLength(256)] - public string? EmailAddress { get; set; } + [StringLength(256)] + public string? EmailAddress { get; init; } - public ICollection SessionsAttendees { get; set; } = - new List(); - } -} \ No newline at end of file + public ICollection SessionsAttendees { get; init; } = + new List(); +} diff --git a/code/session-3/GraphQL/Data/Session.cs b/code/session-3/GraphQL/Data/Session.cs index b340977..086d57a 100644 --- a/code/session-3/GraphQL/Data/Session.cs +++ b/code/session-3/GraphQL/Data/Session.cs @@ -1,37 +1,33 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Session { - public class Session - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? Title { get; set; } + [StringLength(200)] + public required string Title { get; init; } - [StringLength(4000)] - public string? Abstract { get; set; } + [StringLength(4000)] + public string? Abstract { get; init; } - public DateTimeOffset? StartTime { get; set; } + public DateTimeOffset? StartTime { get; set; } - public DateTimeOffset? EndTime { get; set; } + public DateTimeOffset? EndTime { get; set; } - // Bonus points to those who can figure out why this is written this way - public TimeSpan Duration => - EndTime?.Subtract(StartTime ?? EndTime ?? DateTimeOffset.MinValue) ?? - TimeSpan.Zero; + // Bonus points to those who can figure out why this is written this way. + public TimeSpan Duration => + EndTime?.Subtract(StartTime ?? EndTime ?? DateTimeOffset.MinValue) ?? + TimeSpan.Zero; - public int? TrackId { get; set; } + public int? TrackId { get; set; } - public ICollection SessionSpeakers { get; set; } = - new List(); + public ICollection SessionSpeakers { get; init; } = + new List(); - public ICollection SessionAttendees { get; set; } = - new List(); + public ICollection SessionAttendees { get; init; } = + new List(); - public Track? Track { get; set; } - } -} \ No newline at end of file + public Track? Track { get; init; } +} diff --git a/code/session-3/GraphQL/Data/SessionAttendee.cs b/code/session-3/GraphQL/Data/SessionAttendee.cs index 089c71a..892d5ae 100644 --- a/code/session-3/GraphQL/Data/SessionAttendee.cs +++ b/code/session-3/GraphQL/Data/SessionAttendee.cs @@ -1,13 +1,12 @@ -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class SessionAttendee { - public class SessionAttendee - { - public int SessionId { get; set; } + public int SessionId { get; init; } - public Session? Session { get; set; } + public Session Session { get; init; } = null!; - public int AttendeeId { get; set; } + public int AttendeeId { get; init; } - public Attendee? Attendee { get; set; } - } -} \ No newline at end of file + public Attendee Attendee { get; init; } = null!; +} diff --git a/code/session-3/GraphQL/Data/SessionSpeaker.cs b/code/session-3/GraphQL/Data/SessionSpeaker.cs index ed83e86..aeebe62 100644 --- a/code/session-3/GraphQL/Data/SessionSpeaker.cs +++ b/code/session-3/GraphQL/Data/SessionSpeaker.cs @@ -1,13 +1,12 @@ -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class SessionSpeaker { - public class SessionSpeaker - { - public int SessionId { get; set; } + public int SessionId { get; init; } - public Session? Session { get; set; } + public Session Session { get; init; } = null!; - public int SpeakerId { get; set; } + public int SpeakerId { get; init; } - public Speaker? Speaker { get; set; } - } -} \ No newline at end of file + public Speaker Speaker { get; init; } = null!; +} diff --git a/code/session-3/GraphQL/Data/Speaker.cs b/code/session-3/GraphQL/Data/Speaker.cs index 0943514..bf47876 100644 --- a/code/session-3/GraphQL/Data/Speaker.cs +++ b/code/session-3/GraphQL/Data/Speaker.cs @@ -1,23 +1,20 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Speaker { - public class Speaker - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? Name { get; set; } + [StringLength(200)] + public required string Name { get; init; } - [StringLength(4000)] - public string? Bio { get; set; } + [StringLength(4000)] + public string? Bio { get; init; } - [StringLength(1000)] - public string? WebSite { get; set; } + [StringLength(1000)] + public string? Website { get; init; } - public ICollection SessionSpeakers { get; set; } = - new List(); - } - } \ No newline at end of file + public ICollection SessionSpeakers { get; init; } = + new List(); +} diff --git a/code/session-3/GraphQL/Data/Track.cs b/code/session-3/GraphQL/Data/Track.cs index f2392b6..51bd27b 100644 --- a/code/session-3/GraphQL/Data/Track.cs +++ b/code/session-3/GraphQL/Data/Track.cs @@ -1,17 +1,14 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Track { - public class Track - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? Name { get; set; } + [StringLength(200)] + public required string Name { get; set; } - public ICollection Sessions { get; set; } = - new List(); - } -} \ No newline at end of file + public ICollection Sessions { get; init; } = + new List(); +} diff --git a/code/session-3/GraphQL/DataLoader/SessionByIdDataLoader.cs b/code/session-3/GraphQL/DataLoader/SessionByIdDataLoader.cs deleted file mode 100644 index dbd675b..0000000 --- a/code/session-3/GraphQL/DataLoader/SessionByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class SessionByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public SessionByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Sessions - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-3/GraphQL/DataLoader/SpeakerByIdDataLoader.cs b/code/session-3/GraphQL/DataLoader/SpeakerByIdDataLoader.cs deleted file mode 100644 index 44d8208..0000000 --- a/code/session-3/GraphQL/DataLoader/SpeakerByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class SpeakerByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public SpeakerByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Speakers - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-3/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs b/code/session-3/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs deleted file mode 100644 index 553f4f5..0000000 --- a/code/session-3/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL -{ - public static class ObjectFieldDescriptorExtensions - { - public static IObjectFieldDescriptor UseDbContext( - this IObjectFieldDescriptor descriptor) - where TDbContext : DbContext - { - return descriptor.UseScopedService( - create: s => s.GetRequiredService>().CreateDbContext(), - disposeAsync: (s, c) => c.DisposeAsync()); - } - } -} \ No newline at end of file diff --git a/code/session-3/GraphQL/Extensions/UseApplicationDbContextAttribute.cs b/code/session-3/GraphQL/Extensions/UseApplicationDbContextAttribute.cs deleted file mode 100644 index 79c9907..0000000 --- a/code/session-3/GraphQL/Extensions/UseApplicationDbContextAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Reflection; -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types; -using HotChocolate.Types.Descriptors; - -namespace ConferencePlanner.GraphQL -{ - public class UseApplicationDbContextAttribute : ObjectFieldDescriptorAttribute - { - public override void OnConfigure( - IDescriptorContext context, - IObjectFieldDescriptor descriptor, - MemberInfo member) - { - descriptor.UseDbContext(); - } - } -} \ No newline at end of file diff --git a/code/session-3/GraphQL/GraphQL.csproj b/code/session-3/GraphQL/GraphQL.csproj index a717e60..97d215a 100644 --- a/code/session-3/GraphQL/GraphQL.csproj +++ b/code/session-3/GraphQL/GraphQL.csproj @@ -1,18 +1,22 @@ - - - - net5.0 - ConferencePlanner.GraphQL - enable - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - + + + + net8.0 + enable + enable + ConferencePlanner.GraphQL + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/code/session-3/GraphQL/Migrations/20201010183502_Initial.cs b/code/session-3/GraphQL/Migrations/20201010183502_Initial.cs deleted file mode 100644 index 69e30fb..0000000 --- a/code/session-3/GraphQL/Migrations/20201010183502_Initial.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace GraphQL.Migrations -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Speakers", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), - Bio = table.Column(type: "TEXT", maxLength: 4000, nullable: true), - WebSite = table.Column(type: "TEXT", maxLength: 1000, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Speakers", x => x.Id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Speakers"); - } - } -} diff --git a/code/session-3/GraphQL/Migrations/20201010183502_Initial.Designer.cs b/code/session-3/GraphQL/Migrations/20240807140835_Initial.Designer.cs similarity index 57% rename from code/session-3/GraphQL/Migrations/20201010183502_Initial.Designer.cs rename to code/session-3/GraphQL/Migrations/20240807140835_Initial.Designer.cs index dc170c1..d508064 100644 --- a/code/session-3/GraphQL/Migrations/20201010183502_Initial.Designer.cs +++ b/code/session-3/GraphQL/Migrations/20240807140835_Initial.Designer.cs @@ -4,37 +4,46 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20201010183502_Initial")] + [Migration("20240807140835_Initial")] partial class Initial { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Bio") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("WebSite") + b.Property("Website") .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(1000)"); b.HasKey("Id"); diff --git a/code/session-3/GraphQL/Migrations/20240807140835_Initial.cs b/code/session-3/GraphQL/Migrations/20240807140835_Initial.cs new file mode 100644 index 0000000..7301c7a --- /dev/null +++ b/code/session-3/GraphQL/Migrations/20240807140835_Initial.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Speakers", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Bio = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + Website = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Speakers", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Speakers"); + } + } +} diff --git a/code/session-6/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs b/code/session-3/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs similarity index 74% rename from code/session-6/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs rename to code/session-3/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs index 2e3d723..75c788f 100644 --- a/code/session-6/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs +++ b/code/session-3/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs @@ -5,47 +5,56 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20201010202211_Refactoring")] + [Migration("20240812080119_Refactoring")] partial class Refactoring { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("EmailAddress") .HasMaxLength(256) - .HasColumnType("TEXT"); + .HasColumnType("character varying(256)"); b.Property("FirstName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("LastName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("UserName") + b.Property("Username") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); - b.HasIndex("UserName") + b.HasIndex("Username") .IsUnique(); b.ToTable("Attendees"); @@ -55,25 +64,27 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Abstract") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("EndTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("StartTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("Title") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("TrackId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("Id"); @@ -85,10 +96,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("AttendeeId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "AttendeeId"); @@ -100,10 +111,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("SpeakerId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "SpeakerId"); @@ -116,20 +127,22 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Bio") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("WebSite") + b.Property("Website") .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(1000)"); b.HasKey("Id"); @@ -140,12 +153,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); diff --git a/code/session-5/GraphQL/Migrations/20201010202211_Refactoring.cs b/code/session-3/GraphQL/Migrations/20240812080119_Refactoring.cs similarity index 68% rename from code/session-5/GraphQL/Migrations/20201010202211_Refactoring.cs rename to code/session-3/GraphQL/Migrations/20240812080119_Refactoring.cs index ffdcfeb..e544f2e 100644 --- a/code/session-5/GraphQL/Migrations/20201010202211_Refactoring.cs +++ b/code/session-3/GraphQL/Migrations/20240812080119_Refactoring.cs @@ -1,22 +1,27 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { + /// public partial class Refactoring : Migration { + /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "Attendees", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - FirstName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - LastName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - UserName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - EmailAddress = table.Column(type: "TEXT", maxLength: 256, nullable: true) + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + FirstName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + LastName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Username = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + EmailAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true) }, constraints: table => { @@ -27,9 +32,9 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Tracks", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", maxLength: 200, nullable: false) + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false) }, constraints: table => { @@ -40,13 +45,13 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Sessions", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Title = table.Column(type: "TEXT", maxLength: 200, nullable: false), - Abstract = table.Column(type: "TEXT", maxLength: 4000, nullable: true), - StartTime = table.Column(type: "TEXT", nullable: true), - EndTime = table.Column(type: "TEXT", nullable: true), - TrackId = table.Column(type: "INTEGER", nullable: true) + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Abstract = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + StartTime = table.Column(type: "timestamp with time zone", nullable: true), + EndTime = table.Column(type: "timestamp with time zone", nullable: true), + TrackId = table.Column(type: "integer", nullable: true) }, constraints: table => { @@ -55,16 +60,15 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "FK_Sessions_Tracks_TrackId", column: x => x.TrackId, principalTable: "Tracks", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); + principalColumn: "Id"); }); migrationBuilder.CreateTable( name: "SessionAttendee", columns: table => new { - SessionId = table.Column(type: "INTEGER", nullable: false), - AttendeeId = table.Column(type: "INTEGER", nullable: false) + SessionId = table.Column(type: "integer", nullable: false), + AttendeeId = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -87,8 +91,8 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "SessionSpeaker", columns: table => new { - SessionId = table.Column(type: "INTEGER", nullable: false), - SpeakerId = table.Column(type: "INTEGER", nullable: false) + SessionId = table.Column(type: "integer", nullable: false), + SpeakerId = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -108,9 +112,9 @@ protected override void Up(MigrationBuilder migrationBuilder) }); migrationBuilder.CreateIndex( - name: "IX_Attendees_UserName", + name: "IX_Attendees_Username", table: "Attendees", - column: "UserName", + column: "Username", unique: true); migrationBuilder.CreateIndex( @@ -129,6 +133,7 @@ protected override void Up(MigrationBuilder migrationBuilder) column: "SpeakerId"); } + /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( diff --git a/code/session-3/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs b/code/session-3/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs index a66dfe1..6cc21a5 100644 --- a/code/session-3/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/code/session-3/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs @@ -4,8 +4,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { [DbContext(typeof(ApplicationDbContext))] partial class ApplicationDbContextModelSnapshot : ModelSnapshot @@ -14,36 +17,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("EmailAddress") .HasMaxLength(256) - .HasColumnType("TEXT"); + .HasColumnType("character varying(256)"); b.Property("FirstName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("LastName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("UserName") + b.Property("Username") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); - b.HasIndex("UserName") + b.HasIndex("Username") .IsUnique(); b.ToTable("Attendees"); @@ -53,25 +61,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Abstract") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("EndTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("StartTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("Title") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("TrackId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("Id"); @@ -83,10 +93,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("AttendeeId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "AttendeeId"); @@ -98,10 +108,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("SpeakerId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "SpeakerId"); @@ -114,20 +124,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Bio") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("WebSite") + b.Property("Website") .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(1000)"); b.HasKey("Id"); @@ -138,12 +150,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); diff --git a/code/session-3/GraphQL/Mutation.cs b/code/session-3/GraphQL/Mutation.cs deleted file mode 100644 index e376686..0000000 --- a/code/session-3/GraphQL/Mutation.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using HotChocolate; - -namespace ConferencePlanner.GraphQL -{ - public class Mutation - { - [UseApplicationDbContext] - public async Task AddSpeakerAsync( - AddSpeakerInput input, - [ScopedService] ApplicationDbContext context) - { - var speaker = new Speaker - { - Name = input.Name, - Bio = input.Bio, - WebSite = input.WebSite - }; - - context.Speakers.Add(speaker); - await context.SaveChangesAsync(); - - return new AddSpeakerPayload(speaker); - } - } -} \ No newline at end of file diff --git a/code/session-3/GraphQL/Program.cs b/code/session-3/GraphQL/Program.cs index c4914c6..dab1e9b 100644 --- a/code/session-3/GraphQL/Program.cs +++ b/code/session-3/GraphQL/Program.cs @@ -1,26 +1,18 @@ -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 ConferencePlanner.GraphQL -{ - 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(); - }); - } -} +using ConferencePlanner.GraphQL.Data; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddDbContext( + options => options.UseNpgsql("Host=127.0.0.1;Username=graphql_workshop;Password=secret")) + .AddGraphQLServer() + .AddGlobalObjectIdentification() + .AddMutationConventions() + .AddGraphQLTypes(); + +var app = builder.Build(); + +app.MapGraphQL(); + +await app.RunWithGraphQLCommandsAsync(args); diff --git a/code/session-3/GraphQL/Properties/launchSettings.json b/code/session-3/GraphQL/Properties/launchSettings.json new file mode 100644 index 0000000..c0d9484 --- /dev/null +++ b/code/session-3/GraphQL/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7000;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/code/session-3/GraphQL/Query.cs b/code/session-3/GraphQL/Query.cs deleted file mode 100644 index 4ed75a8..0000000 --- a/code/session-3/GraphQL/Query.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; - -namespace ConferencePlanner.GraphQL -{ - public class Query - { - [UseApplicationDbContext] - public Task> GetSpeakers([ScopedService] ApplicationDbContext context) => - context.Speakers.ToListAsync(); - - public Task GetSpeakerAsync( - int id, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - dataLoader.LoadAsync(id, cancellationToken); - } -} \ No newline at end of file diff --git a/code/session-3/GraphQL/Sessions/AddSessionInput.cs b/code/session-3/GraphQL/Sessions/AddSessionInput.cs new file mode 100644 index 0000000..3474bf3 --- /dev/null +++ b/code/session-3/GraphQL/Sessions/AddSessionInput.cs @@ -0,0 +1,8 @@ +using ConferencePlanner.GraphQL.Data; + +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed record AddSessionInput( + string Title, + string? Abstract, + [property: ID] IReadOnlyList SpeakerIds); diff --git a/code/session-3/GraphQL/Sessions/ScheduleSessionInput.cs b/code/session-3/GraphQL/Sessions/ScheduleSessionInput.cs new file mode 100644 index 0000000..9c4fd11 --- /dev/null +++ b/code/session-3/GraphQL/Sessions/ScheduleSessionInput.cs @@ -0,0 +1,9 @@ +using ConferencePlanner.GraphQL.Data; + +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed record ScheduleSessionInput( + [property: ID] int SessionId, + [property: ID] int TrackId, + DateTimeOffset StartTime, + DateTimeOffset EndTime); diff --git a/code/session-3/GraphQL/Sessions/SessionDataLoaders.cs b/code/session-3/GraphQL/Sessions/SessionDataLoaders.cs new file mode 100644 index 0000000..738ce5e --- /dev/null +++ b/code/session-3/GraphQL/Sessions/SessionDataLoaders.cs @@ -0,0 +1,50 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Sessions; + +public static class SessionDataLoaders +{ + [DataLoader] + public static async Task> SessionByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => ids.Contains(s.Id)) + .Select(s => s.Id, selector) + .ToDictionaryAsync(s => s.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SpeakersBySessionIdAsync( + IReadOnlyList sessionIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => sessionIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionSpeakers.Select(ss => ss.Speaker), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } + + [DataLoader] + public static async Task> AttendeesBySessionIdAsync( + IReadOnlyList sessionIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => sessionIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionAttendees.Select(sa => sa.Attendee), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-3/GraphQL/Sessions/SessionExceptions.cs b/code/session-3/GraphQL/Sessions/SessionExceptions.cs new file mode 100644 index 0000000..fea5d77 --- /dev/null +++ b/code/session-3/GraphQL/Sessions/SessionExceptions.cs @@ -0,0 +1,9 @@ +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed class EndTimeInvalidException() : Exception("EndTime must be after StartTime."); + +public sealed class NoSpeakerException() : Exception("No speaker assigned."); + +public sealed class SessionNotFoundException() : Exception("Session not found."); + +public sealed class TitleEmptyException() : Exception("The title cannot be empty."); diff --git a/code/session-3/GraphQL/Sessions/SessionMutations.cs b/code/session-3/GraphQL/Sessions/SessionMutations.cs new file mode 100644 index 0000000..d220f3f --- /dev/null +++ b/code/session-3/GraphQL/Sessions/SessionMutations.cs @@ -0,0 +1,73 @@ +using ConferencePlanner.GraphQL.Data; + +namespace ConferencePlanner.GraphQL.Sessions; + +[MutationType] +public static class SessionMutations +{ + [Error] + [Error] + public static async Task AddSessionAsync( + AddSessionInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(input.Title)) + { + throw new TitleEmptyException(); + } + + if (input.SpeakerIds.Count == 0) + { + throw new NoSpeakerException(); + } + + var session = new Session + { + Title = input.Title, + Abstract = input.Abstract + }; + + foreach (var speakerId in input.SpeakerIds) + { + session.SessionSpeakers.Add(new SessionSpeaker + { + SpeakerId = speakerId + }); + } + + dbContext.Sessions.Add(session); + + await dbContext.SaveChangesAsync(cancellationToken); + + return session; + } + + [Error] + [Error] + public static async Task ScheduleSessionAsync( + ScheduleSessionInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + if (input.EndTime < input.StartTime) + { + throw new EndTimeInvalidException(); + } + + var session = await dbContext.Sessions.FindAsync([input.SessionId], cancellationToken); + + if (session is null) + { + throw new SessionNotFoundException(); + } + + session.TrackId = input.TrackId; + session.StartTime = input.StartTime; + session.EndTime = input.EndTime; + + await dbContext.SaveChangesAsync(cancellationToken); + + return session; + } +} diff --git a/code/session-3/GraphQL/Sessions/SessionQueries.cs b/code/session-3/GraphQL/Sessions/SessionQueries.cs new file mode 100644 index 0000000..799c8c2 --- /dev/null +++ b/code/session-3/GraphQL/Sessions/SessionQueries.cs @@ -0,0 +1,36 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Sessions; + +[QueryType] +public static class SessionQueries +{ + public static async Task> GetSessionsAsync( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + return await dbContext.Sessions.AsNoTracking().ToListAsync(cancellationToken); + } + + [NodeResolver] + public static async Task GetSessionByIdAsync( + int id, + ISessionByIdDataLoader sessionById, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionById.Select(selection).LoadAsync(id, cancellationToken); + } + + public static async Task> GetSessionsByIdAsync( + [ID] int[] ids, + ISessionByIdDataLoader sessionById, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionById.Select(selection).LoadRequiredAsync(ids, cancellationToken); + } +} diff --git a/code/session-3/GraphQL/Sessions/SessionType.cs b/code/session-3/GraphQL/Sessions/SessionType.cs new file mode 100644 index 0000000..abda805 --- /dev/null +++ b/code/session-3/GraphQL/Sessions/SessionType.cs @@ -0,0 +1,60 @@ +using ConferencePlanner.GraphQL.Data; +using ConferencePlanner.GraphQL.Tracks; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Sessions; + +[ObjectType] +public static partial class SessionType +{ + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field(s => s.TrackId) + .ID(); + } + + public static TimeSpan Duration([Parent("StartTime EndTime")] Session session) + => session.Duration; + + [BindMember(nameof(Session.SessionSpeakers))] + public static async Task> GetSpeakersAsync( + [Parent] Session session, + ISpeakersBySessionIdDataLoader speakersBySessionId, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakersBySessionId + .Select(selection) + .LoadRequiredAsync(session.Id, cancellationToken); + } + + [BindMember(nameof(Session.SessionAttendees))] + public static async Task> GetAttendeesAsync( + [Parent(nameof(Session.Id))] Session session, + IAttendeesBySessionIdDataLoader attendeesBySessionId, + ISelection selection, + CancellationToken cancellationToken) + { + return await attendeesBySessionId + .Select(selection) + .LoadRequiredAsync(session.Id, cancellationToken); + } + + public static async Task GetTrackAsync( + [Parent(nameof(Session.TrackId))] Session session, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + if (session.TrackId is null) + { + return null; + } + + return await trackById + .Select(selection) + .LoadAsync(session.TrackId.Value, cancellationToken); + } +} diff --git a/code/session-3/GraphQL/Speakers/AddSpeakerInput.cs b/code/session-3/GraphQL/Speakers/AddSpeakerInput.cs new file mode 100644 index 0000000..bdc584a --- /dev/null +++ b/code/session-3/GraphQL/Speakers/AddSpeakerInput.cs @@ -0,0 +1,6 @@ +namespace ConferencePlanner.GraphQL.Speakers; + +public sealed record AddSpeakerInput( + string Name, + string? Bio, + string? Website); diff --git a/code/session-3/GraphQL/Speakers/SpeakerDataLoaders.cs b/code/session-3/GraphQL/Speakers/SpeakerDataLoaders.cs new file mode 100644 index 0000000..c2c748e --- /dev/null +++ b/code/session-3/GraphQL/Speakers/SpeakerDataLoaders.cs @@ -0,0 +1,36 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Speakers; + +public static class SpeakerDataLoaders +{ + [DataLoader] + public static async Task> SpeakerByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Speakers + .AsNoTracking() + .Where(s => ids.Contains(s.Id)) + .Select(s => s.Id, selector) + .ToDictionaryAsync(s => s.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SessionsBySpeakerIdAsync( + IReadOnlyList speakerIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Speakers + .AsNoTracking() + .Where(s => speakerIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionSpeakers.Select(ss => ss.Session), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-3/GraphQL/Speakers/SpeakerMutations.cs b/code/session-3/GraphQL/Speakers/SpeakerMutations.cs new file mode 100644 index 0000000..0a8ad7a --- /dev/null +++ b/code/session-3/GraphQL/Speakers/SpeakerMutations.cs @@ -0,0 +1,26 @@ +using ConferencePlanner.GraphQL.Data; + +namespace ConferencePlanner.GraphQL.Speakers; + +[MutationType] +public static class SpeakerMutations +{ + public static async Task AddSpeakerAsync( + AddSpeakerInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var speaker = new Speaker + { + Name = input.Name, + Bio = input.Bio, + Website = input.Website + }; + + dbContext.Speakers.Add(speaker); + + await dbContext.SaveChangesAsync(cancellationToken); + + return speaker; + } +} diff --git a/code/session-3/GraphQL/Speakers/SpeakerQueries.cs b/code/session-3/GraphQL/Speakers/SpeakerQueries.cs new file mode 100644 index 0000000..bf5b048 --- /dev/null +++ b/code/session-3/GraphQL/Speakers/SpeakerQueries.cs @@ -0,0 +1,36 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Speakers; + +[QueryType] +public static class SpeakerQueries +{ + public static async Task> GetSpeakersAsync( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + return await dbContext.Speakers.AsNoTracking().ToListAsync(cancellationToken); + } + + [NodeResolver] + public static async Task GetSpeakerByIdAsync( + int id, + ISpeakerByIdDataLoader speakerById, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakerById.Select(selection).LoadAsync(id, cancellationToken); + } + + public static async Task> GetSpeakersByIdAsync( + [ID] int[] ids, + ISpeakerByIdDataLoader speakerById, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakerById.Select(selection).LoadRequiredAsync(ids, cancellationToken); + } +} diff --git a/code/session-3/GraphQL/Speakers/SpeakerType.cs b/code/session-3/GraphQL/Speakers/SpeakerType.cs new file mode 100644 index 0000000..54555fd --- /dev/null +++ b/code/session-3/GraphQL/Speakers/SpeakerType.cs @@ -0,0 +1,21 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Speakers; + +[ObjectType] +public static partial class SpeakerType +{ + [BindMember(nameof(Speaker.SessionSpeakers))] + public static async Task> GetSessionsAsync( + [Parent] Speaker speaker, + ISessionsBySpeakerIdDataLoader sessionsBySpeakerId, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsBySpeakerId + .Select(selection) + .LoadRequiredAsync(speaker.Id, cancellationToken); + } +} diff --git a/code/session-3/GraphQL/Startup.cs b/code/session-3/GraphQL/Startup.cs deleted file mode 100644 index f27f30d..0000000 --- a/code/session-3/GraphQL/Startup.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using ConferencePlanner.GraphQL.Types; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace ConferencePlanner.GraphQL -{ - 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.AddPooledDbContextFactory( - options => options.UseSqlite("Data Source=conferences.db")); - - services - .AddGraphQLServer() - .AddQueryType() - .AddMutationType() - .AddType() - .AddDataLoader() - .AddDataLoader(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGraphQL(); - }); - } - } -} diff --git a/code/session-3/GraphQL/Tracks/AddTrackInput.cs b/code/session-3/GraphQL/Tracks/AddTrackInput.cs new file mode 100644 index 0000000..1aaf313 --- /dev/null +++ b/code/session-3/GraphQL/Tracks/AddTrackInput.cs @@ -0,0 +1,3 @@ +namespace ConferencePlanner.GraphQL.Tracks; + +public sealed record AddTrackInput(string Name); diff --git a/code/session-3/GraphQL/Tracks/RenameTrackInput.cs b/code/session-3/GraphQL/Tracks/RenameTrackInput.cs new file mode 100644 index 0000000..d11ad39 --- /dev/null +++ b/code/session-3/GraphQL/Tracks/RenameTrackInput.cs @@ -0,0 +1,5 @@ +using ConferencePlanner.GraphQL.Data; + +namespace ConferencePlanner.GraphQL.Tracks; + +public sealed record RenameTrackInput([property: ID] int Id, string Name); diff --git a/code/session-3/GraphQL/Tracks/TrackDataLoaders.cs b/code/session-3/GraphQL/Tracks/TrackDataLoaders.cs new file mode 100644 index 0000000..92a2fed --- /dev/null +++ b/code/session-3/GraphQL/Tracks/TrackDataLoaders.cs @@ -0,0 +1,36 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Tracks; + +public static class TrackDataLoaders +{ + [DataLoader] + public static async Task> TrackByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Tracks + .AsNoTracking() + .Where(t => ids.Contains(t.Id)) + .Select(t => t.Id, selector) + .ToDictionaryAsync(t => t.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SessionsByTrackIdAsync( + IReadOnlyList trackIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Tracks + .AsNoTracking() + .Where(t => trackIds.Contains(t.Id)) + .Select(t => t.Id, t => t.Sessions, selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-3/GraphQL/Tracks/TrackExceptions.cs b/code/session-3/GraphQL/Tracks/TrackExceptions.cs new file mode 100644 index 0000000..8df488d --- /dev/null +++ b/code/session-3/GraphQL/Tracks/TrackExceptions.cs @@ -0,0 +1,3 @@ +namespace ConferencePlanner.GraphQL.Tracks; + +public sealed class TrackNotFoundException() : Exception("Track not found."); diff --git a/code/session-3/GraphQL/Tracks/TrackMutations.cs b/code/session-3/GraphQL/Tracks/TrackMutations.cs new file mode 100644 index 0000000..a91671c --- /dev/null +++ b/code/session-3/GraphQL/Tracks/TrackMutations.cs @@ -0,0 +1,41 @@ +using ConferencePlanner.GraphQL.Data; + +namespace ConferencePlanner.GraphQL.Tracks; + +[MutationType] +public static class TrackMutations +{ + public static async Task AddTrackAsync( + AddTrackInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var track = new Track { Name = input.Name }; + + dbContext.Tracks.Add(track); + + await dbContext.SaveChangesAsync(cancellationToken); + + return track; + } + + [Error] + public static async Task RenameTrackAsync( + RenameTrackInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var track = await dbContext.Tracks.FindAsync([input.Id], cancellationToken); + + if (track is null) + { + throw new TrackNotFoundException(); + } + + track.Name = input.Name; + + await dbContext.SaveChangesAsync(cancellationToken); + + return track; + } +} diff --git a/code/session-3/GraphQL/Tracks/TrackQueries.cs b/code/session-3/GraphQL/Tracks/TrackQueries.cs new file mode 100644 index 0000000..9522d4e --- /dev/null +++ b/code/session-3/GraphQL/Tracks/TrackQueries.cs @@ -0,0 +1,36 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Tracks; + +[QueryType] +public static class TrackQueries +{ + public static async Task> GetTracksAsync( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + return await dbContext.Tracks.AsNoTracking().ToListAsync(cancellationToken); + } + + [NodeResolver] + public static async Task GetTrackByIdAsync( + int id, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + return await trackById.Select(selection).LoadAsync(id, cancellationToken); + } + + public static async Task> GetTracksByIdAsync( + [ID] int[] ids, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + return await trackById.Select(selection).LoadRequiredAsync(ids, cancellationToken); + } +} diff --git a/code/session-3/GraphQL/Tracks/TrackType.cs b/code/session-3/GraphQL/Tracks/TrackType.cs new file mode 100644 index 0000000..22d35e8 --- /dev/null +++ b/code/session-3/GraphQL/Tracks/TrackType.cs @@ -0,0 +1,20 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Tracks; + +[ObjectType] +public static partial class TrackType +{ + public static async Task> GetSessionsAsync( + [Parent] Track track, + ISessionsByTrackIdDataLoader sessionsByTrackId, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsByTrackId + .Select(selection) + .LoadRequiredAsync(track.Id, cancellationToken); + } +} diff --git a/code/session-3/GraphQL/Types/SpeakerType.cs b/code/session-3/GraphQL/Types/SpeakerType.cs deleted file mode 100644 index 6924356..0000000 --- a/code/session-3/GraphQL/Types/SpeakerType.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Types -{ - public class SpeakerType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .Field(t => t.SessionSpeakers) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("sessions"); - } - - private class SpeakerResolvers - { - public async Task> GetSessionsAsync( - Speaker speaker, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Speakers - .Where(s => s.Id == speaker.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SessionId)) - .ToArrayAsync(); - - return await sessionById.LoadAsync(speakerIds, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-3/GraphQL/appsettings.Development.json b/code/session-3/GraphQL/appsettings.Development.json index dba68eb..0c208ae 100644 --- a/code/session-3/GraphQL/appsettings.Development.json +++ b/code/session-3/GraphQL/appsettings.Development.json @@ -1,9 +1,8 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Information", + "Microsoft.AspNetCore": "Warning" } } } diff --git a/code/session-3/GraphQL/appsettings.json b/code/session-3/GraphQL/appsettings.json index 81ff877..10f68b8 100644 --- a/code/session-3/GraphQL/appsettings.json +++ b/code/session-3/GraphQL/appsettings.json @@ -1,10 +1,9 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/code/session-3/docker-compose.yml b/code/session-3/docker-compose.yml new file mode 100644 index 0000000..728458d --- /dev/null +++ b/code/session-3/docker-compose.yml @@ -0,0 +1,23 @@ +name: graphql-workshop + +services: + graphql-workshop-postgres: + container_name: graphql-workshop-postgres + image: postgres:17.2 + environment: + POSTGRES_USER: graphql_workshop + POSTGRES_PASSWORD: secret + POSTGRES_DB: graphql_workshop + networks: [graphql-workshop] + ports: ["5432:5432"] + volumes: + - type: volume + source: postgres-data + target: /var/lib/postgresql/data + +networks: + graphql-workshop: + name: graphql-workshop + +volumes: + postgres-data: diff --git a/code/session-4/.config/dotnet-tools.json b/code/session-4/.config/dotnet-tools.json index c735fef..aad8137 100644 --- a/code/session-4/.config/dotnet-tools.json +++ b/code/session-4/.config/dotnet-tools.json @@ -3,10 +3,11 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "5.0.0", + "version": "9.0.1", "commands": [ "dotnet-ef" - ] + ], + "rollForward": false } } -} \ No newline at end of file +} diff --git a/code/session-4/.vscode/launch.json b/code/session-4/.vscode/launch.json deleted file mode 100644 index e90375e..0000000 --- a/code/session-4/.vscode/launch.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - // 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}/GraphQL/bin/Debug/net5.0/GraphQL.dll", - "args": [], - "cwd": "${workspaceFolder}/GraphQL", - "stopAtEntry": false, - "serverReadyAction": { - "action": "openExternally", - "pattern": "\\bNow 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/code/session-4/.vscode/tasks.json b/code/session-4/.vscode/tasks.json deleted file mode 100644 index 31c32bd..0000000 --- a/code/session-4/.vscode/tasks.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - // 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/code/session-4/ConferencePlanner.sln b/code/session-4/ConferencePlanner.sln index 42fadb8..8c414fc 100644 --- a/code/session-4/ConferencePlanner.sln +++ b/code/session-4/ConferencePlanner.sln @@ -1,34 +1,22 @@ - -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}") = "GraphQL", "GraphQL\GraphQL.csproj", "{48385280-56F1-4937-9655-E6A79184740B}" -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 - {48385280-56F1-4937-9655-E6A79184740B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x64.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x64.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x86.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x86.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|Any CPU.Build.0 = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x64.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x64.Build.0 = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x86.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL", "GraphQL\GraphQL.csproj", "{D96823B9-86D3-4D54-A803-F1D43AEBE1FD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/code/session-4/GraphQL/Attendees/AttendeeDataLoaders.cs b/code/session-4/GraphQL/Attendees/AttendeeDataLoaders.cs new file mode 100644 index 0000000..56b8e21 --- /dev/null +++ b/code/session-4/GraphQL/Attendees/AttendeeDataLoaders.cs @@ -0,0 +1,36 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Attendees; + +public static class AttendeeDataLoaders +{ + [DataLoader] + public static async Task> AttendeeByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Attendees + .AsNoTracking() + .Where(a => ids.Contains(a.Id)) + .Select(a => a.Id, selector) + .ToDictionaryAsync(a => a.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SessionsByAttendeeIdAsync( + IReadOnlyList attendeeIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Attendees + .AsNoTracking() + .Where(a => attendeeIds.Contains(a.Id)) + .Select(a => a.Id, a => a.SessionsAttendees.Select(sa => sa.Session), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-4/GraphQL/Attendees/AttendeeType.cs b/code/session-4/GraphQL/Attendees/AttendeeType.cs new file mode 100644 index 0000000..a76e76a --- /dev/null +++ b/code/session-4/GraphQL/Attendees/AttendeeType.cs @@ -0,0 +1,32 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Attendees; + +[ObjectType] +public static partial class AttendeeType +{ + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .ImplementsNode() + .IdField(a => a.Id) + .ResolveNode( + async (ctx, id) + => await ctx.DataLoader() + .LoadAsync(id, ctx.RequestAborted)); + } + + [BindMember(nameof(Attendee.SessionsAttendees))] + public static async Task> GetSessionsAsync( + [Parent] Attendee attendee, + ISessionsByAttendeeIdDataLoader sessionsByAttendeeId, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsByAttendeeId + .Select(selection) + .LoadRequiredAsync(attendee.Id, cancellationToken); + } +} diff --git a/code/session-4/GraphQL/Common/Payload.cs b/code/session-4/GraphQL/Common/Payload.cs deleted file mode 100644 index e9d2839..0000000 --- a/code/session-4/GraphQL/Common/Payload.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace ConferencePlanner.GraphQL.Common -{ - public abstract class Payload - { - protected Payload(IReadOnlyList? errors = null) - { - Errors = errors; - } - - public IReadOnlyList? Errors { get; } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/Common/UserError.cs b/code/session-4/GraphQL/Common/UserError.cs deleted file mode 100644 index 3d587dd..0000000 --- a/code/session-4/GraphQL/Common/UserError.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace ConferencePlanner.GraphQL.Common -{ - public class UserError - { - public UserError(string message, string code) - { - Message = message; - Code = code; - } - - public string Message { get; } - - public string Code { get; } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/Data/ApplicationDbContext.cs b/code/session-4/GraphQL/Data/ApplicationDbContext.cs index bbd7dda..5a2d633 100644 --- a/code/session-4/GraphQL/Data/ApplicationDbContext.cs +++ b/code/session-4/GraphQL/Data/ApplicationDbContext.cs @@ -1,38 +1,33 @@ - using Microsoft.EntityFrameworkCore; - - namespace ConferencePlanner.GraphQL.Data - { - public class ApplicationDbContext : DbContext - { - public ApplicationDbContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasIndex(a => a.UserName) - .IsUnique(); - - // Many-to-many: Session <-> Attendee - modelBuilder - .Entity() - .HasKey(ca => new { ca.SessionId, ca.AttendeeId }); - - // Many-to-many: Speaker <-> Session - modelBuilder - .Entity() - .HasKey(ss => new { ss.SessionId, ss.SpeakerId }); - } - - public DbSet Sessions { get; set; } = default!; - - public DbSet Tracks { get; set; } = default!; - - public DbSet Speakers { get; set; } = default!; - - public DbSet Attendees { get; set; } = default!; - } - } \ No newline at end of file +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Data; + +public sealed class ApplicationDbContext(DbContextOptions options) + : DbContext(options) +{ + public DbSet Attendees { get; init; } + + public DbSet Sessions { get; init; } + + public DbSet Speakers { get; init; } + + public DbSet Tracks { get; init; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .HasIndex(a => a.Username) + .IsUnique(); + + // Many-to-many: Session <-> Attendee + modelBuilder + .Entity() + .HasKey(sa => new { sa.SessionId, sa.AttendeeId }); + + // Many-to-many: Speaker <-> Session + modelBuilder + .Entity() + .HasKey(ss => new { ss.SessionId, ss.SpeakerId }); + } +} diff --git a/code/session-4/GraphQL/Data/Attendee.cs b/code/session-4/GraphQL/Data/Attendee.cs index e3f9ab0..304ec00 100644 --- a/code/session-4/GraphQL/Data/Attendee.cs +++ b/code/session-4/GraphQL/Data/Attendee.cs @@ -1,28 +1,23 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Attendee { - public class Attendee - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? FirstName { get; set; } + [StringLength(200)] + public required string FirstName { get; init; } - [Required] - [StringLength(200)] - public string? LastName { get; set; } + [StringLength(200)] + public required string LastName { get; init; } - [Required] - [StringLength(200)] - public string? UserName { get; set; } + [StringLength(200)] + public required string Username { get; init; } - [StringLength(256)] - public string? EmailAddress { get; set; } + [StringLength(256)] + public string? EmailAddress { get; init; } - public ICollection SessionsAttendees { get; set; } = - new List(); - } -} \ No newline at end of file + public ICollection SessionsAttendees { get; init; } = + new List(); +} diff --git a/code/session-4/GraphQL/Data/Session.cs b/code/session-4/GraphQL/Data/Session.cs index b340977..086d57a 100644 --- a/code/session-4/GraphQL/Data/Session.cs +++ b/code/session-4/GraphQL/Data/Session.cs @@ -1,37 +1,33 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Session { - public class Session - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? Title { get; set; } + [StringLength(200)] + public required string Title { get; init; } - [StringLength(4000)] - public string? Abstract { get; set; } + [StringLength(4000)] + public string? Abstract { get; init; } - public DateTimeOffset? StartTime { get; set; } + public DateTimeOffset? StartTime { get; set; } - public DateTimeOffset? EndTime { get; set; } + public DateTimeOffset? EndTime { get; set; } - // Bonus points to those who can figure out why this is written this way - public TimeSpan Duration => - EndTime?.Subtract(StartTime ?? EndTime ?? DateTimeOffset.MinValue) ?? - TimeSpan.Zero; + // Bonus points to those who can figure out why this is written this way. + public TimeSpan Duration => + EndTime?.Subtract(StartTime ?? EndTime ?? DateTimeOffset.MinValue) ?? + TimeSpan.Zero; - public int? TrackId { get; set; } + public int? TrackId { get; set; } - public ICollection SessionSpeakers { get; set; } = - new List(); + public ICollection SessionSpeakers { get; init; } = + new List(); - public ICollection SessionAttendees { get; set; } = - new List(); + public ICollection SessionAttendees { get; init; } = + new List(); - public Track? Track { get; set; } - } -} \ No newline at end of file + public Track? Track { get; init; } +} diff --git a/code/session-4/GraphQL/Data/SessionAttendee.cs b/code/session-4/GraphQL/Data/SessionAttendee.cs index 089c71a..892d5ae 100644 --- a/code/session-4/GraphQL/Data/SessionAttendee.cs +++ b/code/session-4/GraphQL/Data/SessionAttendee.cs @@ -1,13 +1,12 @@ -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class SessionAttendee { - public class SessionAttendee - { - public int SessionId { get; set; } + public int SessionId { get; init; } - public Session? Session { get; set; } + public Session Session { get; init; } = null!; - public int AttendeeId { get; set; } + public int AttendeeId { get; init; } - public Attendee? Attendee { get; set; } - } -} \ No newline at end of file + public Attendee Attendee { get; init; } = null!; +} diff --git a/code/session-4/GraphQL/Data/SessionSpeaker.cs b/code/session-4/GraphQL/Data/SessionSpeaker.cs index ed83e86..aeebe62 100644 --- a/code/session-4/GraphQL/Data/SessionSpeaker.cs +++ b/code/session-4/GraphQL/Data/SessionSpeaker.cs @@ -1,13 +1,12 @@ -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class SessionSpeaker { - public class SessionSpeaker - { - public int SessionId { get; set; } + public int SessionId { get; init; } - public Session? Session { get; set; } + public Session Session { get; init; } = null!; - public int SpeakerId { get; set; } + public int SpeakerId { get; init; } - public Speaker? Speaker { get; set; } - } -} \ No newline at end of file + public Speaker Speaker { get; init; } = null!; +} diff --git a/code/session-4/GraphQL/Data/Speaker.cs b/code/session-4/GraphQL/Data/Speaker.cs index 0943514..bf47876 100644 --- a/code/session-4/GraphQL/Data/Speaker.cs +++ b/code/session-4/GraphQL/Data/Speaker.cs @@ -1,23 +1,20 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Speaker { - public class Speaker - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? Name { get; set; } + [StringLength(200)] + public required string Name { get; init; } - [StringLength(4000)] - public string? Bio { get; set; } + [StringLength(4000)] + public string? Bio { get; init; } - [StringLength(1000)] - public string? WebSite { get; set; } + [StringLength(1000)] + public string? Website { get; init; } - public ICollection SessionSpeakers { get; set; } = - new List(); - } - } \ No newline at end of file + public ICollection SessionSpeakers { get; init; } = + new List(); +} diff --git a/code/session-4/GraphQL/Data/Track.cs b/code/session-4/GraphQL/Data/Track.cs index f2392b6..51bd27b 100644 --- a/code/session-4/GraphQL/Data/Track.cs +++ b/code/session-4/GraphQL/Data/Track.cs @@ -1,17 +1,14 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Track { - public class Track - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? Name { get; set; } + [StringLength(200)] + public required string Name { get; set; } - public ICollection Sessions { get; set; } = - new List(); - } -} \ No newline at end of file + public ICollection Sessions { get; init; } = + new List(); +} diff --git a/code/session-4/GraphQL/DataLoader/AttendeeByIdDataLoader.cs b/code/session-4/GraphQL/DataLoader/AttendeeByIdDataLoader.cs deleted file mode 100644 index a2bdbd0..0000000 --- a/code/session-4/GraphQL/DataLoader/AttendeeByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class AttendeeByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public AttendeeByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Attendees - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/DataLoader/SessionByIdDataLoader.cs b/code/session-4/GraphQL/DataLoader/SessionByIdDataLoader.cs deleted file mode 100644 index dbd675b..0000000 --- a/code/session-4/GraphQL/DataLoader/SessionByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class SessionByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public SessionByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Sessions - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/DataLoader/SpeakerByIdDataLoader.cs b/code/session-4/GraphQL/DataLoader/SpeakerByIdDataLoader.cs deleted file mode 100644 index 44d8208..0000000 --- a/code/session-4/GraphQL/DataLoader/SpeakerByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class SpeakerByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public SpeakerByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Speakers - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/DataLoader/TrackByIdDataLoader.cs b/code/session-4/GraphQL/DataLoader/TrackByIdDataLoader.cs deleted file mode 100644 index 4db1f95..0000000 --- a/code/session-4/GraphQL/DataLoader/TrackByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class TrackByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public TrackByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Tracks - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs b/code/session-4/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs index 553f4f5..49c13a8 100644 --- a/code/session-4/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs +++ b/code/session-4/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs @@ -1,18 +1,17 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using HotChocolate.Types; +namespace ConferencePlanner.GraphQL.Extensions; -namespace ConferencePlanner.GraphQL +public static class ObjectFieldDescriptorExtensions { - public static class ObjectFieldDescriptorExtensions + public static IObjectFieldDescriptor UseUpperCase(this IObjectFieldDescriptor descriptor) { - public static IObjectFieldDescriptor UseDbContext( - this IObjectFieldDescriptor descriptor) - where TDbContext : DbContext + return descriptor.Use(next => async context => { - return descriptor.UseScopedService( - create: s => s.GetRequiredService>().CreateDbContext(), - disposeAsync: (s, c) => c.DisposeAsync()); - } + await next(context); + + if (context.Result is string s) + { + context.Result = s.ToUpperInvariant(); + } + }); } -} \ No newline at end of file +} diff --git a/code/session-4/GraphQL/Extensions/UseApplicationDbContextAttribute.cs b/code/session-4/GraphQL/Extensions/UseApplicationDbContextAttribute.cs deleted file mode 100644 index 79c9907..0000000 --- a/code/session-4/GraphQL/Extensions/UseApplicationDbContextAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Reflection; -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types; -using HotChocolate.Types.Descriptors; - -namespace ConferencePlanner.GraphQL -{ - public class UseApplicationDbContextAttribute : ObjectFieldDescriptorAttribute - { - public override void OnConfigure( - IDescriptorContext context, - IObjectFieldDescriptor descriptor, - MemberInfo member) - { - descriptor.UseDbContext(); - } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/Extensions/UseUpperCaseAttribute.cs b/code/session-4/GraphQL/Extensions/UseUpperCaseAttribute.cs new file mode 100644 index 0000000..b85152d --- /dev/null +++ b/code/session-4/GraphQL/Extensions/UseUpperCaseAttribute.cs @@ -0,0 +1,15 @@ +using System.Reflection; +using HotChocolate.Types.Descriptors; + +namespace ConferencePlanner.GraphQL.Extensions; + +public sealed class UseUpperCaseAttribute : ObjectFieldDescriptorAttribute +{ + protected override void OnConfigure( + IDescriptorContext context, + IObjectFieldDescriptor descriptor, + MemberInfo member) + { + descriptor.UseUpperCase(); + } +} diff --git a/code/session-4/GraphQL/GraphQL.csproj b/code/session-4/GraphQL/GraphQL.csproj index a717e60..97d215a 100644 --- a/code/session-4/GraphQL/GraphQL.csproj +++ b/code/session-4/GraphQL/GraphQL.csproj @@ -1,18 +1,22 @@ - - - - net5.0 - ConferencePlanner.GraphQL - enable - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - + + + + net8.0 + enable + enable + ConferencePlanner.GraphQL + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/code/session-4/GraphQL/Migrations/20201010183502_Initial.cs b/code/session-4/GraphQL/Migrations/20201010183502_Initial.cs deleted file mode 100644 index 69e30fb..0000000 --- a/code/session-4/GraphQL/Migrations/20201010183502_Initial.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace GraphQL.Migrations -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Speakers", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), - Bio = table.Column(type: "TEXT", maxLength: 4000, nullable: true), - WebSite = table.Column(type: "TEXT", maxLength: 1000, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Speakers", x => x.Id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Speakers"); - } - } -} diff --git a/code/session-1/GraphQL/Migrations/20201010183502_Initial.Designer.cs b/code/session-4/GraphQL/Migrations/20240807140835_Initial.Designer.cs similarity index 57% rename from code/session-1/GraphQL/Migrations/20201010183502_Initial.Designer.cs rename to code/session-4/GraphQL/Migrations/20240807140835_Initial.Designer.cs index dc170c1..d508064 100644 --- a/code/session-1/GraphQL/Migrations/20201010183502_Initial.Designer.cs +++ b/code/session-4/GraphQL/Migrations/20240807140835_Initial.Designer.cs @@ -4,37 +4,46 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20201010183502_Initial")] + [Migration("20240807140835_Initial")] partial class Initial { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Bio") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("WebSite") + b.Property("Website") .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(1000)"); b.HasKey("Id"); diff --git a/code/session-4/GraphQL/Migrations/20240807140835_Initial.cs b/code/session-4/GraphQL/Migrations/20240807140835_Initial.cs new file mode 100644 index 0000000..7301c7a --- /dev/null +++ b/code/session-4/GraphQL/Migrations/20240807140835_Initial.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Speakers", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Bio = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + Website = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Speakers", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Speakers"); + } + } +} diff --git a/code/session-4/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs b/code/session-4/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs similarity index 74% rename from code/session-4/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs rename to code/session-4/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs index 2e3d723..75c788f 100644 --- a/code/session-4/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs +++ b/code/session-4/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs @@ -5,47 +5,56 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20201010202211_Refactoring")] + [Migration("20240812080119_Refactoring")] partial class Refactoring { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("EmailAddress") .HasMaxLength(256) - .HasColumnType("TEXT"); + .HasColumnType("character varying(256)"); b.Property("FirstName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("LastName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("UserName") + b.Property("Username") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); - b.HasIndex("UserName") + b.HasIndex("Username") .IsUnique(); b.ToTable("Attendees"); @@ -55,25 +64,27 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Abstract") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("EndTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("StartTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("Title") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("TrackId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("Id"); @@ -85,10 +96,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("AttendeeId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "AttendeeId"); @@ -100,10 +111,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("SpeakerId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "SpeakerId"); @@ -116,20 +127,22 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Bio") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("WebSite") + b.Property("Website") .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(1000)"); b.HasKey("Id"); @@ -140,12 +153,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); diff --git a/code/session-6/GraphQL/Migrations/20201010202211_Refactoring.cs b/code/session-4/GraphQL/Migrations/20240812080119_Refactoring.cs similarity index 68% rename from code/session-6/GraphQL/Migrations/20201010202211_Refactoring.cs rename to code/session-4/GraphQL/Migrations/20240812080119_Refactoring.cs index ffdcfeb..e544f2e 100644 --- a/code/session-6/GraphQL/Migrations/20201010202211_Refactoring.cs +++ b/code/session-4/GraphQL/Migrations/20240812080119_Refactoring.cs @@ -1,22 +1,27 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { + /// public partial class Refactoring : Migration { + /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "Attendees", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - FirstName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - LastName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - UserName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - EmailAddress = table.Column(type: "TEXT", maxLength: 256, nullable: true) + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + FirstName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + LastName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Username = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + EmailAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true) }, constraints: table => { @@ -27,9 +32,9 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Tracks", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", maxLength: 200, nullable: false) + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false) }, constraints: table => { @@ -40,13 +45,13 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Sessions", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Title = table.Column(type: "TEXT", maxLength: 200, nullable: false), - Abstract = table.Column(type: "TEXT", maxLength: 4000, nullable: true), - StartTime = table.Column(type: "TEXT", nullable: true), - EndTime = table.Column(type: "TEXT", nullable: true), - TrackId = table.Column(type: "INTEGER", nullable: true) + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Abstract = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + StartTime = table.Column(type: "timestamp with time zone", nullable: true), + EndTime = table.Column(type: "timestamp with time zone", nullable: true), + TrackId = table.Column(type: "integer", nullable: true) }, constraints: table => { @@ -55,16 +60,15 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "FK_Sessions_Tracks_TrackId", column: x => x.TrackId, principalTable: "Tracks", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); + principalColumn: "Id"); }); migrationBuilder.CreateTable( name: "SessionAttendee", columns: table => new { - SessionId = table.Column(type: "INTEGER", nullable: false), - AttendeeId = table.Column(type: "INTEGER", nullable: false) + SessionId = table.Column(type: "integer", nullable: false), + AttendeeId = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -87,8 +91,8 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "SessionSpeaker", columns: table => new { - SessionId = table.Column(type: "INTEGER", nullable: false), - SpeakerId = table.Column(type: "INTEGER", nullable: false) + SessionId = table.Column(type: "integer", nullable: false), + SpeakerId = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -108,9 +112,9 @@ protected override void Up(MigrationBuilder migrationBuilder) }); migrationBuilder.CreateIndex( - name: "IX_Attendees_UserName", + name: "IX_Attendees_Username", table: "Attendees", - column: "UserName", + column: "Username", unique: true); migrationBuilder.CreateIndex( @@ -129,6 +133,7 @@ protected override void Up(MigrationBuilder migrationBuilder) column: "SpeakerId"); } + /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( diff --git a/code/session-4/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs b/code/session-4/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs index a66dfe1..6cc21a5 100644 --- a/code/session-4/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/code/session-4/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs @@ -4,8 +4,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { [DbContext(typeof(ApplicationDbContext))] partial class ApplicationDbContextModelSnapshot : ModelSnapshot @@ -14,36 +17,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("EmailAddress") .HasMaxLength(256) - .HasColumnType("TEXT"); + .HasColumnType("character varying(256)"); b.Property("FirstName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("LastName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("UserName") + b.Property("Username") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); - b.HasIndex("UserName") + b.HasIndex("Username") .IsUnique(); b.ToTable("Attendees"); @@ -53,25 +61,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Abstract") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("EndTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("StartTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("Title") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("TrackId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("Id"); @@ -83,10 +93,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("AttendeeId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "AttendeeId"); @@ -98,10 +108,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("SpeakerId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "SpeakerId"); @@ -114,20 +124,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Bio") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("WebSite") + b.Property("Website") .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(1000)"); b.HasKey("Id"); @@ -138,12 +150,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); diff --git a/code/session-4/GraphQL/Program.cs b/code/session-4/GraphQL/Program.cs index c4914c6..dab1e9b 100644 --- a/code/session-4/GraphQL/Program.cs +++ b/code/session-4/GraphQL/Program.cs @@ -1,26 +1,18 @@ -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 ConferencePlanner.GraphQL -{ - 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(); - }); - } -} +using ConferencePlanner.GraphQL.Data; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddDbContext( + options => options.UseNpgsql("Host=127.0.0.1;Username=graphql_workshop;Password=secret")) + .AddGraphQLServer() + .AddGlobalObjectIdentification() + .AddMutationConventions() + .AddGraphQLTypes(); + +var app = builder.Build(); + +app.MapGraphQL(); + +await app.RunWithGraphQLCommandsAsync(args); diff --git a/code/session-4/GraphQL/Properties/launchSettings.json b/code/session-4/GraphQL/Properties/launchSettings.json new file mode 100644 index 0000000..c0d9484 --- /dev/null +++ b/code/session-4/GraphQL/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7000;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/code/session-4/GraphQL/Sessions/AddSessionInput.cs b/code/session-4/GraphQL/Sessions/AddSessionInput.cs index db5995f..3474bf3 100644 --- a/code/session-4/GraphQL/Sessions/AddSessionInput.cs +++ b/code/session-4/GraphQL/Sessions/AddSessionInput.cs @@ -1,12 +1,8 @@ -using System.Collections.Generic; using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; -namespace ConferencePlanner.GraphQL.Sessions -{ - public record AddSessionInput( - string Title, - string? Abstract, - [ID(nameof(Speaker))] - IReadOnlyList SpeakerIds); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed record AddSessionInput( + string Title, + string? Abstract, + [property: ID] IReadOnlyList SpeakerIds); diff --git a/code/session-4/GraphQL/Sessions/AddSessionPayload.cs b/code/session-4/GraphQL/Sessions/AddSessionPayload.cs deleted file mode 100644 index 82775f8..0000000 --- a/code/session-4/GraphQL/Sessions/AddSessionPayload.cs +++ /dev/null @@ -1,20 +0,0 @@ -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class AddSessionPayload : Payload - { - public AddSessionPayload(Session session) - { - Session = session; - } - - public AddSessionPayload(UserError error) - : base(new[] { error }) - { - } - - public Session? Session { get; init; } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/Sessions/ScheduleSessionInput.cs b/code/session-4/GraphQL/Sessions/ScheduleSessionInput.cs index fc43463..9c4fd11 100644 --- a/code/session-4/GraphQL/Sessions/ScheduleSessionInput.cs +++ b/code/session-4/GraphQL/Sessions/ScheduleSessionInput.cs @@ -1,14 +1,9 @@ -using System; using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; -namespace ConferencePlanner.GraphQL.Sessions -{ - public record ScheduleSessionInput( - [ID(nameof(Session))] - int SessionId, - [ID(nameof(Track))] - int TrackId, - DateTimeOffset StartTime, - DateTimeOffset EndTime); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed record ScheduleSessionInput( + [property: ID] int SessionId, + [property: ID] int TrackId, + DateTimeOffset StartTime, + DateTimeOffset EndTime); diff --git a/code/session-4/GraphQL/Sessions/ScheduleSessionPayload.cs b/code/session-4/GraphQL/Sessions/ScheduleSessionPayload.cs deleted file mode 100644 index ce79df5..0000000 --- a/code/session-4/GraphQL/Sessions/ScheduleSessionPayload.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class ScheduleSessionPayload : SessionPayloadBase - { - public ScheduleSessionPayload(Session session) - : base(session) - { - } - - public ScheduleSessionPayload(UserError error) - : base(new[] { error }) - { - } - - public async Task GetTrackAsync( - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) - { - if (Session is null) - { - return null; - } - - return await trackById.LoadAsync(Session.Id, cancellationToken); - } - - [UseApplicationDbContext] - public async Task?> GetSpeakersAsync( - [ScopedService] ApplicationDbContext dbContext, - SpeakerByIdDataLoader speakerById, - CancellationToken cancellationToken) - { - if (Session is null) - { - return null; - } - - int[] speakerIds = await dbContext.Sessions - .Where(s => s.Id == Session.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SpeakerId)) - .ToArrayAsync(); - - return await speakerById.LoadAsync(speakerIds, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/Sessions/SessionDataLoaders.cs b/code/session-4/GraphQL/Sessions/SessionDataLoaders.cs new file mode 100644 index 0000000..738ce5e --- /dev/null +++ b/code/session-4/GraphQL/Sessions/SessionDataLoaders.cs @@ -0,0 +1,50 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Sessions; + +public static class SessionDataLoaders +{ + [DataLoader] + public static async Task> SessionByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => ids.Contains(s.Id)) + .Select(s => s.Id, selector) + .ToDictionaryAsync(s => s.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SpeakersBySessionIdAsync( + IReadOnlyList sessionIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => sessionIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionSpeakers.Select(ss => ss.Speaker), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } + + [DataLoader] + public static async Task> AttendeesBySessionIdAsync( + IReadOnlyList sessionIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => sessionIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionAttendees.Select(sa => sa.Attendee), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-4/GraphQL/Sessions/SessionExceptions.cs b/code/session-4/GraphQL/Sessions/SessionExceptions.cs new file mode 100644 index 0000000..fea5d77 --- /dev/null +++ b/code/session-4/GraphQL/Sessions/SessionExceptions.cs @@ -0,0 +1,9 @@ +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed class EndTimeInvalidException() : Exception("EndTime must be after StartTime."); + +public sealed class NoSpeakerException() : Exception("No speaker assigned."); + +public sealed class SessionNotFoundException() : Exception("Session not found."); + +public sealed class TitleEmptyException() : Exception("The title cannot be empty."); diff --git a/code/session-4/GraphQL/Sessions/SessionMutations.cs b/code/session-4/GraphQL/Sessions/SessionMutations.cs index 74ca479..d220f3f 100644 --- a/code/session-4/GraphQL/Sessions/SessionMutations.cs +++ b/code/session-4/GraphQL/Sessions/SessionMutations.cs @@ -1,80 +1,73 @@ -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Common; using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; -namespace ConferencePlanner.GraphQL.Sessions +namespace ConferencePlanner.GraphQL.Sessions; + +[MutationType] +public static class SessionMutations { - [ExtendObjectType(Name = "Mutation")] - public class SessionMutations + [Error] + [Error] + public static async Task AddSessionAsync( + AddSessionInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - [UseApplicationDbContext] - public async Task AddSessionAsync( - AddSessionInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) + if (string.IsNullOrEmpty(input.Title)) { - if (string.IsNullOrEmpty(input.Title)) - { - return new AddSessionPayload( - new UserError("The title cannot be empty.", "TITLE_EMPTY")); - } + throw new TitleEmptyException(); + } - if (input.SpeakerIds.Count == 0) - { - return new AddSessionPayload( - new UserError("No speaker assigned.", "NO_SPEAKER")); - } + if (input.SpeakerIds.Count == 0) + { + throw new NoSpeakerException(); + } - var session = new Session - { - Title = input.Title, - Abstract = input.Abstract, - }; + var session = new Session + { + Title = input.Title, + Abstract = input.Abstract + }; - foreach (int speakerId in input.SpeakerIds) + foreach (var speakerId in input.SpeakerIds) + { + session.SessionSpeakers.Add(new SessionSpeaker { - session.SessionSpeakers.Add(new SessionSpeaker - { - SpeakerId = speakerId - }); - } + SpeakerId = speakerId + }); + } - context.Sessions.Add(session); - await context.SaveChangesAsync(cancellationToken); + dbContext.Sessions.Add(session); - return new AddSessionPayload(session); - } + await dbContext.SaveChangesAsync(cancellationToken); + + return session; + } - [UseApplicationDbContext] - public async Task ScheduleSessionAsync( - ScheduleSessionInput input, - [ScopedService] ApplicationDbContext context) + [Error] + [Error] + public static async Task ScheduleSessionAsync( + ScheduleSessionInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + if (input.EndTime < input.StartTime) { - if (input.EndTime < input.StartTime) - { - return new ScheduleSessionPayload( - new UserError("endTime has to be larger than startTime.", "END_TIME_INVALID")); - } + throw new EndTimeInvalidException(); + } - Session session = await context.Sessions.FindAsync(input.SessionId); - int? initialTrackId = session.TrackId; + var session = await dbContext.Sessions.FindAsync([input.SessionId], cancellationToken); - if (session is null) - { - return new ScheduleSessionPayload( - new UserError("Session not found.", "SESSION_NOT_FOUND")); - } + if (session is null) + { + throw new SessionNotFoundException(); + } - session.TrackId = input.TrackId; - session.StartTime = input.StartTime; - session.EndTime = input.EndTime; + session.TrackId = input.TrackId; + session.StartTime = input.StartTime; + session.EndTime = input.EndTime; - await context.SaveChangesAsync(); + await dbContext.SaveChangesAsync(cancellationToken); - return new ScheduleSessionPayload(session); - } + return session; } -} \ No newline at end of file +} diff --git a/code/session-4/GraphQL/Sessions/SessionPayloadBase.cs b/code/session-4/GraphQL/Sessions/SessionPayloadBase.cs deleted file mode 100644 index 888ad50..0000000 --- a/code/session-4/GraphQL/Sessions/SessionPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class SessionPayloadBase : Payload - { - protected SessionPayloadBase(Session session) - { - Session = session; - } - - protected SessionPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Session? Session { get; } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/Sessions/SessionQueries.cs b/code/session-4/GraphQL/Sessions/SessionQueries.cs index 9e8428d..799c8c2 100644 --- a/code/session-4/GraphQL/Sessions/SessionQueries.cs +++ b/code/session-4/GraphQL/Sessions/SessionQueries.cs @@ -1,34 +1,36 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; -namespace ConferencePlanner.GraphQL.Sessions +namespace ConferencePlanner.GraphQL.Sessions; + +[QueryType] +public static class SessionQueries { - [ExtendObjectType(Name = "Query")] - public class SessionQueries + public static async Task> GetSessionsAsync( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - [UseApplicationDbContext] - public async Task> GetSessionsAsync( - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - await context.Sessions.ToListAsync(cancellationToken); + return await dbContext.Sessions.AsNoTracking().ToListAsync(cancellationToken); + } - public Task GetSessionByIdAsync( - [ID(nameof(Session))] int id, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - sessionById.LoadAsync(id, cancellationToken); + [NodeResolver] + public static async Task GetSessionByIdAsync( + int id, + ISessionByIdDataLoader sessionById, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionById.Select(selection).LoadAsync(id, cancellationToken); + } - public async Task> GetSessionsByIdAsync( - [ID(nameof(Session))] int[] ids, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - await sessionById.LoadAsync(ids, cancellationToken); + public static async Task> GetSessionsByIdAsync( + [ID] int[] ids, + ISessionByIdDataLoader sessionById, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionById.Select(selection).LoadRequiredAsync(ids, cancellationToken); } -} \ No newline at end of file +} diff --git a/code/session-4/GraphQL/Sessions/SessionType.cs b/code/session-4/GraphQL/Sessions/SessionType.cs new file mode 100644 index 0000000..abda805 --- /dev/null +++ b/code/session-4/GraphQL/Sessions/SessionType.cs @@ -0,0 +1,60 @@ +using ConferencePlanner.GraphQL.Data; +using ConferencePlanner.GraphQL.Tracks; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Sessions; + +[ObjectType] +public static partial class SessionType +{ + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field(s => s.TrackId) + .ID(); + } + + public static TimeSpan Duration([Parent("StartTime EndTime")] Session session) + => session.Duration; + + [BindMember(nameof(Session.SessionSpeakers))] + public static async Task> GetSpeakersAsync( + [Parent] Session session, + ISpeakersBySessionIdDataLoader speakersBySessionId, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakersBySessionId + .Select(selection) + .LoadRequiredAsync(session.Id, cancellationToken); + } + + [BindMember(nameof(Session.SessionAttendees))] + public static async Task> GetAttendeesAsync( + [Parent(nameof(Session.Id))] Session session, + IAttendeesBySessionIdDataLoader attendeesBySessionId, + ISelection selection, + CancellationToken cancellationToken) + { + return await attendeesBySessionId + .Select(selection) + .LoadRequiredAsync(session.Id, cancellationToken); + } + + public static async Task GetTrackAsync( + [Parent(nameof(Session.TrackId))] Session session, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + if (session.TrackId is null) + { + return null; + } + + return await trackById + .Select(selection) + .LoadAsync(session.TrackId.Value, cancellationToken); + } +} diff --git a/code/session-4/GraphQL/Speakers/AddSpeakerInput.cs b/code/session-4/GraphQL/Speakers/AddSpeakerInput.cs index a81f45f..bdc584a 100644 --- a/code/session-4/GraphQL/Speakers/AddSpeakerInput.cs +++ b/code/session-4/GraphQL/Speakers/AddSpeakerInput.cs @@ -1,7 +1,6 @@ -namespace ConferencePlanner.GraphQL.Speakers -{ - public record AddSpeakerInput( - string Name, - string? Bio, - string? WebSite); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Speakers; + +public sealed record AddSpeakerInput( + string Name, + string? Bio, + string? Website); diff --git a/code/session-4/GraphQL/Speakers/AddSpeakerPayload.cs b/code/session-4/GraphQL/Speakers/AddSpeakerPayload.cs deleted file mode 100644 index aaf0ab0..0000000 --- a/code/session-4/GraphQL/Speakers/AddSpeakerPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Speakers -{ - public class AddSpeakerPayload : SpeakerPayloadBase - { - public AddSpeakerPayload(Speaker speaker) - : base(speaker) - { - } - - public AddSpeakerPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/Speakers/SpeakerDataLoaders.cs b/code/session-4/GraphQL/Speakers/SpeakerDataLoaders.cs new file mode 100644 index 0000000..c2c748e --- /dev/null +++ b/code/session-4/GraphQL/Speakers/SpeakerDataLoaders.cs @@ -0,0 +1,36 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Speakers; + +public static class SpeakerDataLoaders +{ + [DataLoader] + public static async Task> SpeakerByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Speakers + .AsNoTracking() + .Where(s => ids.Contains(s.Id)) + .Select(s => s.Id, selector) + .ToDictionaryAsync(s => s.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SessionsBySpeakerIdAsync( + IReadOnlyList speakerIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Speakers + .AsNoTracking() + .Where(s => speakerIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionSpeakers.Select(ss => ss.Session), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-4/GraphQL/Speakers/SpeakerMutations.cs b/code/session-4/GraphQL/Speakers/SpeakerMutations.cs index 55fc6f5..0a8ad7a 100644 --- a/code/session-4/GraphQL/Speakers/SpeakerMutations.cs +++ b/code/session-4/GraphQL/Speakers/SpeakerMutations.cs @@ -1,29 +1,26 @@ -using System.Threading.Tasks; using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; -namespace ConferencePlanner.GraphQL.Speakers +namespace ConferencePlanner.GraphQL.Speakers; + +[MutationType] +public static class SpeakerMutations { - [ExtendObjectType(Name = "Mutation")] - public class SpeakerMutations + public static async Task AddSpeakerAsync( + AddSpeakerInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - [UseApplicationDbContext] - public async Task AddSpeakerAsync( - AddSpeakerInput input, - [ScopedService] ApplicationDbContext context) + var speaker = new Speaker { - var speaker = new Speaker - { - Name = input.Name, - Bio = input.Bio, - WebSite = input.WebSite - }; + Name = input.Name, + Bio = input.Bio, + Website = input.Website + }; + + dbContext.Speakers.Add(speaker); - context.Speakers.Add(speaker); - await context.SaveChangesAsync(); + await dbContext.SaveChangesAsync(cancellationToken); - return new AddSpeakerPayload(speaker); - } + return speaker; } -} \ No newline at end of file +} diff --git a/code/session-4/GraphQL/Speakers/SpeakerPayloadBase.cs b/code/session-4/GraphQL/Speakers/SpeakerPayloadBase.cs deleted file mode 100644 index a2077d7..0000000 --- a/code/session-4/GraphQL/Speakers/SpeakerPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Speakers -{ - public class SpeakerPayloadBase : Payload - { - protected SpeakerPayloadBase(Speaker speaker) - { - Speaker = speaker; - } - - protected SpeakerPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Speaker? Speaker { get; init; } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/Speakers/SpeakerQueries.cs b/code/session-4/GraphQL/Speakers/SpeakerQueries.cs index 69eb814..bf5b048 100644 --- a/code/session-4/GraphQL/Speakers/SpeakerQueries.cs +++ b/code/session-4/GraphQL/Speakers/SpeakerQueries.cs @@ -1,32 +1,36 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; -namespace ConferencePlanner.GraphQL.Speakers +namespace ConferencePlanner.GraphQL.Speakers; + +[QueryType] +public static class SpeakerQueries { - [ExtendObjectType(Name = "Query")] - public class SpeakerQueries + public static async Task> GetSpeakersAsync( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - [UseApplicationDbContext] - public Task> GetSpeakers([ScopedService] ApplicationDbContext context) => - context.Speakers.ToListAsync(); + return await dbContext.Speakers.AsNoTracking().ToListAsync(cancellationToken); + } - public Task GetSpeakerByIdAsync( - [ID(nameof(Speaker))]int id, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - dataLoader.LoadAsync(id, cancellationToken); + [NodeResolver] + public static async Task GetSpeakerByIdAsync( + int id, + ISpeakerByIdDataLoader speakerById, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakerById.Select(selection).LoadAsync(id, cancellationToken); + } - public async Task> GetSpeakersByIdAsync( - [ID(nameof(Speaker))]int[] ids, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - await dataLoader.LoadAsync(ids, cancellationToken); + public static async Task> GetSpeakersByIdAsync( + [ID] int[] ids, + ISpeakerByIdDataLoader speakerById, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakerById.Select(selection).LoadRequiredAsync(ids, cancellationToken); } -} \ No newline at end of file +} diff --git a/code/session-4/GraphQL/Speakers/SpeakerType.cs b/code/session-4/GraphQL/Speakers/SpeakerType.cs new file mode 100644 index 0000000..54555fd --- /dev/null +++ b/code/session-4/GraphQL/Speakers/SpeakerType.cs @@ -0,0 +1,21 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Speakers; + +[ObjectType] +public static partial class SpeakerType +{ + [BindMember(nameof(Speaker.SessionSpeakers))] + public static async Task> GetSessionsAsync( + [Parent] Speaker speaker, + ISessionsBySpeakerIdDataLoader sessionsBySpeakerId, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsBySpeakerId + .Select(selection) + .LoadRequiredAsync(speaker.Id, cancellationToken); + } +} diff --git a/code/session-4/GraphQL/Startup.cs b/code/session-4/GraphQL/Startup.cs deleted file mode 100644 index 683d4e0..0000000 --- a/code/session-4/GraphQL/Startup.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using ConferencePlanner.GraphQL.Sessions; -using ConferencePlanner.GraphQL.Speakers; -using ConferencePlanner.GraphQL.Tracks; -using ConferencePlanner.GraphQL.Types; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace ConferencePlanner.GraphQL -{ - 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.AddPooledDbContextFactory( - options => options.UseSqlite("Data Source=conferences.db")); - - services - .AddGraphQLServer() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport() - .AddDataLoader() - .AddDataLoader(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGraphQL(); - }); - } - } -} diff --git a/code/session-4/GraphQL/Tracks/AddTrackInput.cs b/code/session-4/GraphQL/Tracks/AddTrackInput.cs index 5c83b34..1aaf313 100644 --- a/code/session-4/GraphQL/Tracks/AddTrackInput.cs +++ b/code/session-4/GraphQL/Tracks/AddTrackInput.cs @@ -1,4 +1,3 @@ -namespace ConferencePlanner.GraphQL.Tracks -{ - public record AddTrackInput(string Name); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Tracks; + +public sealed record AddTrackInput(string Name); diff --git a/code/session-4/GraphQL/Tracks/AddTrackPayload.cs b/code/session-4/GraphQL/Tracks/AddTrackPayload.cs deleted file mode 100644 index 8f35b13..0000000 --- a/code/session-4/GraphQL/Tracks/AddTrackPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class AddTrackPayload : TrackPayloadBase - { - public AddTrackPayload(Track track) - : base(track) - { - } - - public AddTrackPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/Tracks/RenameTrackInput.cs b/code/session-4/GraphQL/Tracks/RenameTrackInput.cs index 516c6a0..d11ad39 100644 --- a/code/session-4/GraphQL/Tracks/RenameTrackInput.cs +++ b/code/session-4/GraphQL/Tracks/RenameTrackInput.cs @@ -1,7 +1,5 @@ using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; -namespace ConferencePlanner.GraphQL.Tracks -{ - public record RenameTrackInput([ID(nameof(Track))] int Id, string Name); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Tracks; + +public sealed record RenameTrackInput([property: ID] int Id, string Name); diff --git a/code/session-4/GraphQL/Tracks/RenameTrackPayload.cs b/code/session-4/GraphQL/Tracks/RenameTrackPayload.cs deleted file mode 100644 index ca4c8a1..0000000 --- a/code/session-4/GraphQL/Tracks/RenameTrackPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class RenameTrackPayload : TrackPayloadBase - { - public RenameTrackPayload(Track track) - : base(track) - { - } - - public RenameTrackPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/Tracks/TrackDataLoaders.cs b/code/session-4/GraphQL/Tracks/TrackDataLoaders.cs new file mode 100644 index 0000000..92a2fed --- /dev/null +++ b/code/session-4/GraphQL/Tracks/TrackDataLoaders.cs @@ -0,0 +1,36 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Tracks; + +public static class TrackDataLoaders +{ + [DataLoader] + public static async Task> TrackByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Tracks + .AsNoTracking() + .Where(t => ids.Contains(t.Id)) + .Select(t => t.Id, selector) + .ToDictionaryAsync(t => t.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SessionsByTrackIdAsync( + IReadOnlyList trackIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Tracks + .AsNoTracking() + .Where(t => trackIds.Contains(t.Id)) + .Select(t => t.Id, t => t.Sessions, selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-4/GraphQL/Tracks/TrackExceptions.cs b/code/session-4/GraphQL/Tracks/TrackExceptions.cs new file mode 100644 index 0000000..8df488d --- /dev/null +++ b/code/session-4/GraphQL/Tracks/TrackExceptions.cs @@ -0,0 +1,3 @@ +namespace ConferencePlanner.GraphQL.Tracks; + +public sealed class TrackNotFoundException() : Exception("Track not found."); diff --git a/code/session-4/GraphQL/Tracks/TrackMutations.cs b/code/session-4/GraphQL/Tracks/TrackMutations.cs index 88727b9..a91671c 100644 --- a/code/session-4/GraphQL/Tracks/TrackMutations.cs +++ b/code/session-4/GraphQL/Tracks/TrackMutations.cs @@ -1,40 +1,41 @@ -using System.Threading; -using System.Threading.Tasks; using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; -namespace ConferencePlanner.GraphQL.Tracks +namespace ConferencePlanner.GraphQL.Tracks; + +[MutationType] +public static class TrackMutations { - [ExtendObjectType(Name = "Mutation")] - public class TrackMutations + public static async Task AddTrackAsync( + AddTrackInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - [UseApplicationDbContext] - public async Task AddTrackAsync( - AddTrackInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - var track = new Track { Name = input.Name }; - context.Tracks.Add(track); + var track = new Track { Name = input.Name }; - await context.SaveChangesAsync(cancellationToken); + dbContext.Tracks.Add(track); - return new AddTrackPayload(track); - } + await dbContext.SaveChangesAsync(cancellationToken); - [UseApplicationDbContext] - public async Task RenameTrackAsync( - RenameTrackInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - Track track = await context.Tracks.FindAsync(input.Id); - track.Name = input.Name; + return track; + } - await context.SaveChangesAsync(cancellationToken); + [Error] + public static async Task RenameTrackAsync( + RenameTrackInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var track = await dbContext.Tracks.FindAsync([input.Id], cancellationToken); - return new RenameTrackPayload(track); + if (track is null) + { + throw new TrackNotFoundException(); } + + track.Name = input.Name; + + await dbContext.SaveChangesAsync(cancellationToken); + + return track; } -} \ No newline at end of file +} diff --git a/code/session-4/GraphQL/Tracks/TrackPayloadBase.cs b/code/session-4/GraphQL/Tracks/TrackPayloadBase.cs deleted file mode 100644 index de11da7..0000000 --- a/code/session-4/GraphQL/Tracks/TrackPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class TrackPayloadBase : Payload - { - public TrackPayloadBase(Track track) - { - Track = track; - } - - public TrackPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Track? Track { get; } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/Tracks/TrackQueries.cs b/code/session-4/GraphQL/Tracks/TrackQueries.cs index dd0288f..9522d4e 100644 --- a/code/session-4/GraphQL/Tracks/TrackQueries.cs +++ b/code/session-4/GraphQL/Tracks/TrackQueries.cs @@ -1,49 +1,36 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Tracks; -namespace ConferencePlanner.GraphQL.Tracks +[QueryType] +public static class TrackQueries { - [ExtendObjectType(Name = "Query")] - public class TrackQueries + public static async Task> GetTracksAsync( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - [UseApplicationDbContext] - public async Task> GetTracksAsync( - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - await context.Tracks.ToListAsync(cancellationToken); - - [UseApplicationDbContext] - public Task GetTrackByNameAsync( - string name, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - context.Tracks.FirstAsync(t => t.Name == name); - - [UseApplicationDbContext] - public async Task> GetTrackByNamesAsync( - string[] names, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - await context.Tracks.Where(t => names.Contains(t.Name)).ToListAsync(); + return await dbContext.Tracks.AsNoTracking().ToListAsync(cancellationToken); + } - public Task GetTrackByIdAsync( - [ID(nameof(Track))] int id, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) => - trackById.LoadAsync(id, cancellationToken); + [NodeResolver] + public static async Task GetTrackByIdAsync( + int id, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + return await trackById.Select(selection).LoadAsync(id, cancellationToken); + } - public async Task> GetTracksByIdAsync( - [ID(nameof(Track))] int[] ids, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) => - await trackById.LoadAsync(ids, cancellationToken); + public static async Task> GetTracksByIdAsync( + [ID] int[] ids, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + return await trackById.Select(selection).LoadRequiredAsync(ids, cancellationToken); } -} \ No newline at end of file +} diff --git a/code/session-4/GraphQL/Tracks/TrackType.cs b/code/session-4/GraphQL/Tracks/TrackType.cs new file mode 100644 index 0000000..5313443 --- /dev/null +++ b/code/session-4/GraphQL/Tracks/TrackType.cs @@ -0,0 +1,29 @@ +using ConferencePlanner.GraphQL.Data; +using ConferencePlanner.GraphQL.Extensions; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Tracks; + +[ObjectType] +public static partial class TrackType +{ + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field(t => t.Name) + .ParentRequires(nameof(Track.Name)) + .UseUpperCase(); + } + + public static async Task> GetSessionsAsync( + [Parent] Track track, + ISessionsByTrackIdDataLoader sessionsByTrackId, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsByTrackId + .Select(selection) + .LoadRequiredAsync(track.Id, cancellationToken); + } +} diff --git a/code/session-4/GraphQL/Types/AttendeeType.cs b/code/session-4/GraphQL/Types/AttendeeType.cs deleted file mode 100644 index 62274c7..0000000 --- a/code/session-4/GraphQL/Types/AttendeeType.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Types -{ - public class AttendeeType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionsAttendees) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("sessions"); - } - - private class AttendeeResolvers - { - public async Task> GetSessionsAsync( - Attendee attendee, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Attendees - .Where(a => a.Id == attendee.Id) - .Include(a => a.SessionsAttendees) - .SelectMany(a => a.SessionsAttendees.Select(t => t.SessionId)) - .ToArrayAsync(); - - return await sessionById.LoadAsync(speakerIds, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/Types/SessionType.cs b/code/session-4/GraphQL/Types/SessionType.cs deleted file mode 100644 index 8a558f7..0000000 --- a/code/session-4/GraphQL/Types/SessionType.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Types -{ - public class SessionType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionSpeakers) - .ResolveWith(t => t.GetSpeakersAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("speakers"); - - descriptor - .Field(t => t.SessionAttendees) - .ResolveWith(t => t.GetAttendeesAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("attendees"); - - descriptor - .Field(t => t.Track) - .ResolveWith(t => t.GetTrackAsync(default!, default!, default)); - - descriptor - .Field(t => t.TrackId) - .ID(nameof(Track)); - } - - private class SessionResolvers - { - public async Task> GetSpeakersAsync( - Session session, - [ScopedService] ApplicationDbContext dbContext, - SpeakerByIdDataLoader speakerById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Sessions - .Where(s => s.Id == session.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SpeakerId)) - .ToArrayAsync(); - - return await speakerById.LoadAsync(speakerIds, cancellationToken); - } - - public async Task> GetAttendeesAsync( - Session session, - [ScopedService] ApplicationDbContext dbContext, - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) - { - int[] attendeeIds = await dbContext.Sessions - .Where(s => s.Id == session.Id) - .Include(session => session.SessionAttendees) - .SelectMany(session => session.SessionAttendees.Select(t => t.AttendeeId)) - .ToArrayAsync(); - - return await attendeeById.LoadAsync(attendeeIds, cancellationToken); - } - - public async Task GetTrackAsync( - Session session, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) - { - if (session.TrackId is null) - { - return null; - } - - return await trackById.LoadAsync(session.TrackId.Value, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/Types/SpeakerType.cs b/code/session-4/GraphQL/Types/SpeakerType.cs deleted file mode 100644 index 5e2b2e7..0000000 --- a/code/session-4/GraphQL/Types/SpeakerType.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Types -{ - public class SpeakerType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionSpeakers) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("sessions"); - } - - private class SpeakerResolvers - { - public async Task> GetSessionsAsync( - Speaker speaker, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Speakers - .Where(s => s.Id == speaker.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SessionId)) - .ToArrayAsync(); - - return await sessionById.LoadAsync(speakerIds, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/Types/TrackType.cs b/code/session-4/GraphQL/Types/TrackType.cs deleted file mode 100644 index e8ee063..0000000 --- a/code/session-4/GraphQL/Types/TrackType.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Types -{ - public class TrackType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => - ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.Sessions) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .UsePaging>() - .Name("sessions"); - } - - private class TrackResolvers - { - public async Task> GetSessionsAsync( - Track track, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] sessionIds = await dbContext.Sessions - .Where(s => s.Id == track.Id) - .Select(s => s.Id) - .ToArrayAsync(); - - return await sessionById.LoadAsync(sessionIds, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-4/GraphQL/appsettings.Development.json b/code/session-4/GraphQL/appsettings.Development.json index dba68eb..0c208ae 100644 --- a/code/session-4/GraphQL/appsettings.Development.json +++ b/code/session-4/GraphQL/appsettings.Development.json @@ -1,9 +1,8 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Information", + "Microsoft.AspNetCore": "Warning" } } } diff --git a/code/session-4/GraphQL/appsettings.json b/code/session-4/GraphQL/appsettings.json index 81ff877..10f68b8 100644 --- a/code/session-4/GraphQL/appsettings.json +++ b/code/session-4/GraphQL/appsettings.json @@ -1,10 +1,9 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/code/session-4/docker-compose.yml b/code/session-4/docker-compose.yml new file mode 100644 index 0000000..728458d --- /dev/null +++ b/code/session-4/docker-compose.yml @@ -0,0 +1,23 @@ +name: graphql-workshop + +services: + graphql-workshop-postgres: + container_name: graphql-workshop-postgres + image: postgres:17.2 + environment: + POSTGRES_USER: graphql_workshop + POSTGRES_PASSWORD: secret + POSTGRES_DB: graphql_workshop + networks: [graphql-workshop] + ports: ["5432:5432"] + volumes: + - type: volume + source: postgres-data + target: /var/lib/postgresql/data + +networks: + graphql-workshop: + name: graphql-workshop + +volumes: + postgres-data: diff --git a/code/session-5/.config/dotnet-tools.json b/code/session-5/.config/dotnet-tools.json index c735fef..aad8137 100644 --- a/code/session-5/.config/dotnet-tools.json +++ b/code/session-5/.config/dotnet-tools.json @@ -3,10 +3,11 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "5.0.0", + "version": "9.0.1", "commands": [ "dotnet-ef" - ] + ], + "rollForward": false } } -} \ No newline at end of file +} diff --git a/code/session-5/.vscode/launch.json b/code/session-5/.vscode/launch.json deleted file mode 100644 index e90375e..0000000 --- a/code/session-5/.vscode/launch.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - // 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}/GraphQL/bin/Debug/net5.0/GraphQL.dll", - "args": [], - "cwd": "${workspaceFolder}/GraphQL", - "stopAtEntry": false, - "serverReadyAction": { - "action": "openExternally", - "pattern": "\\bNow 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/code/session-5/.vscode/tasks.json b/code/session-5/.vscode/tasks.json deleted file mode 100644 index 31c32bd..0000000 --- a/code/session-5/.vscode/tasks.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - // 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/code/session-5/ConferencePlanner.sln b/code/session-5/ConferencePlanner.sln index 42fadb8..8c414fc 100644 --- a/code/session-5/ConferencePlanner.sln +++ b/code/session-5/ConferencePlanner.sln @@ -1,34 +1,22 @@ - -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}") = "GraphQL", "GraphQL\GraphQL.csproj", "{48385280-56F1-4937-9655-E6A79184740B}" -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 - {48385280-56F1-4937-9655-E6A79184740B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x64.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x64.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x86.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x86.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|Any CPU.Build.0 = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x64.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x64.Build.0 = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x86.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL", "GraphQL\GraphQL.csproj", "{D96823B9-86D3-4D54-A803-F1D43AEBE1FD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/code/session-5/GraphQL/Attendees/AttendeeDataLoaders.cs b/code/session-5/GraphQL/Attendees/AttendeeDataLoaders.cs new file mode 100644 index 0000000..56b8e21 --- /dev/null +++ b/code/session-5/GraphQL/Attendees/AttendeeDataLoaders.cs @@ -0,0 +1,36 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Attendees; + +public static class AttendeeDataLoaders +{ + [DataLoader] + public static async Task> AttendeeByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Attendees + .AsNoTracking() + .Where(a => ids.Contains(a.Id)) + .Select(a => a.Id, selector) + .ToDictionaryAsync(a => a.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SessionsByAttendeeIdAsync( + IReadOnlyList attendeeIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Attendees + .AsNoTracking() + .Where(a => attendeeIds.Contains(a.Id)) + .Select(a => a.Id, a => a.SessionsAttendees.Select(sa => sa.Session), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-5/GraphQL/Attendees/AttendeeType.cs b/code/session-5/GraphQL/Attendees/AttendeeType.cs new file mode 100644 index 0000000..a76e76a --- /dev/null +++ b/code/session-5/GraphQL/Attendees/AttendeeType.cs @@ -0,0 +1,32 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Attendees; + +[ObjectType] +public static partial class AttendeeType +{ + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .ImplementsNode() + .IdField(a => a.Id) + .ResolveNode( + async (ctx, id) + => await ctx.DataLoader() + .LoadAsync(id, ctx.RequestAborted)); + } + + [BindMember(nameof(Attendee.SessionsAttendees))] + public static async Task> GetSessionsAsync( + [Parent] Attendee attendee, + ISessionsByAttendeeIdDataLoader sessionsByAttendeeId, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsByAttendeeId + .Select(selection) + .LoadRequiredAsync(attendee.Id, cancellationToken); + } +} diff --git a/code/session-5/GraphQL/Common/Payload.cs b/code/session-5/GraphQL/Common/Payload.cs deleted file mode 100644 index e9d2839..0000000 --- a/code/session-5/GraphQL/Common/Payload.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace ConferencePlanner.GraphQL.Common -{ - public abstract class Payload - { - protected Payload(IReadOnlyList? errors = null) - { - Errors = errors; - } - - public IReadOnlyList? Errors { get; } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/Common/UserError.cs b/code/session-5/GraphQL/Common/UserError.cs deleted file mode 100644 index 3d587dd..0000000 --- a/code/session-5/GraphQL/Common/UserError.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace ConferencePlanner.GraphQL.Common -{ - public class UserError - { - public UserError(string message, string code) - { - Message = message; - Code = code; - } - - public string Message { get; } - - public string Code { get; } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/Data/ApplicationDbContext.cs b/code/session-5/GraphQL/Data/ApplicationDbContext.cs index bbd7dda..5a2d633 100644 --- a/code/session-5/GraphQL/Data/ApplicationDbContext.cs +++ b/code/session-5/GraphQL/Data/ApplicationDbContext.cs @@ -1,38 +1,33 @@ - using Microsoft.EntityFrameworkCore; - - namespace ConferencePlanner.GraphQL.Data - { - public class ApplicationDbContext : DbContext - { - public ApplicationDbContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasIndex(a => a.UserName) - .IsUnique(); - - // Many-to-many: Session <-> Attendee - modelBuilder - .Entity() - .HasKey(ca => new { ca.SessionId, ca.AttendeeId }); - - // Many-to-many: Speaker <-> Session - modelBuilder - .Entity() - .HasKey(ss => new { ss.SessionId, ss.SpeakerId }); - } - - public DbSet Sessions { get; set; } = default!; - - public DbSet Tracks { get; set; } = default!; - - public DbSet Speakers { get; set; } = default!; - - public DbSet Attendees { get; set; } = default!; - } - } \ No newline at end of file +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Data; + +public sealed class ApplicationDbContext(DbContextOptions options) + : DbContext(options) +{ + public DbSet Attendees { get; init; } + + public DbSet Sessions { get; init; } + + public DbSet Speakers { get; init; } + + public DbSet Tracks { get; init; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .HasIndex(a => a.Username) + .IsUnique(); + + // Many-to-many: Session <-> Attendee + modelBuilder + .Entity() + .HasKey(sa => new { sa.SessionId, sa.AttendeeId }); + + // Many-to-many: Speaker <-> Session + modelBuilder + .Entity() + .HasKey(ss => new { ss.SessionId, ss.SpeakerId }); + } +} diff --git a/code/session-5/GraphQL/Data/Attendee.cs b/code/session-5/GraphQL/Data/Attendee.cs index e3f9ab0..304ec00 100644 --- a/code/session-5/GraphQL/Data/Attendee.cs +++ b/code/session-5/GraphQL/Data/Attendee.cs @@ -1,28 +1,23 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Attendee { - public class Attendee - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? FirstName { get; set; } + [StringLength(200)] + public required string FirstName { get; init; } - [Required] - [StringLength(200)] - public string? LastName { get; set; } + [StringLength(200)] + public required string LastName { get; init; } - [Required] - [StringLength(200)] - public string? UserName { get; set; } + [StringLength(200)] + public required string Username { get; init; } - [StringLength(256)] - public string? EmailAddress { get; set; } + [StringLength(256)] + public string? EmailAddress { get; init; } - public ICollection SessionsAttendees { get; set; } = - new List(); - } -} \ No newline at end of file + public ICollection SessionsAttendees { get; init; } = + new List(); +} diff --git a/code/session-5/GraphQL/Data/Session.cs b/code/session-5/GraphQL/Data/Session.cs index b340977..086d57a 100644 --- a/code/session-5/GraphQL/Data/Session.cs +++ b/code/session-5/GraphQL/Data/Session.cs @@ -1,37 +1,33 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Session { - public class Session - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? Title { get; set; } + [StringLength(200)] + public required string Title { get; init; } - [StringLength(4000)] - public string? Abstract { get; set; } + [StringLength(4000)] + public string? Abstract { get; init; } - public DateTimeOffset? StartTime { get; set; } + public DateTimeOffset? StartTime { get; set; } - public DateTimeOffset? EndTime { get; set; } + public DateTimeOffset? EndTime { get; set; } - // Bonus points to those who can figure out why this is written this way - public TimeSpan Duration => - EndTime?.Subtract(StartTime ?? EndTime ?? DateTimeOffset.MinValue) ?? - TimeSpan.Zero; + // Bonus points to those who can figure out why this is written this way. + public TimeSpan Duration => + EndTime?.Subtract(StartTime ?? EndTime ?? DateTimeOffset.MinValue) ?? + TimeSpan.Zero; - public int? TrackId { get; set; } + public int? TrackId { get; set; } - public ICollection SessionSpeakers { get; set; } = - new List(); + public ICollection SessionSpeakers { get; init; } = + new List(); - public ICollection SessionAttendees { get; set; } = - new List(); + public ICollection SessionAttendees { get; init; } = + new List(); - public Track? Track { get; set; } - } -} \ No newline at end of file + public Track? Track { get; init; } +} diff --git a/code/session-5/GraphQL/Data/SessionAttendee.cs b/code/session-5/GraphQL/Data/SessionAttendee.cs index 089c71a..892d5ae 100644 --- a/code/session-5/GraphQL/Data/SessionAttendee.cs +++ b/code/session-5/GraphQL/Data/SessionAttendee.cs @@ -1,13 +1,12 @@ -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class SessionAttendee { - public class SessionAttendee - { - public int SessionId { get; set; } + public int SessionId { get; init; } - public Session? Session { get; set; } + public Session Session { get; init; } = null!; - public int AttendeeId { get; set; } + public int AttendeeId { get; init; } - public Attendee? Attendee { get; set; } - } -} \ No newline at end of file + public Attendee Attendee { get; init; } = null!; +} diff --git a/code/session-5/GraphQL/Data/SessionSpeaker.cs b/code/session-5/GraphQL/Data/SessionSpeaker.cs index ed83e86..aeebe62 100644 --- a/code/session-5/GraphQL/Data/SessionSpeaker.cs +++ b/code/session-5/GraphQL/Data/SessionSpeaker.cs @@ -1,13 +1,12 @@ -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class SessionSpeaker { - public class SessionSpeaker - { - public int SessionId { get; set; } + public int SessionId { get; init; } - public Session? Session { get; set; } + public Session Session { get; init; } = null!; - public int SpeakerId { get; set; } + public int SpeakerId { get; init; } - public Speaker? Speaker { get; set; } - } -} \ No newline at end of file + public Speaker Speaker { get; init; } = null!; +} diff --git a/code/session-5/GraphQL/Data/Speaker.cs b/code/session-5/GraphQL/Data/Speaker.cs index 0943514..bf47876 100644 --- a/code/session-5/GraphQL/Data/Speaker.cs +++ b/code/session-5/GraphQL/Data/Speaker.cs @@ -1,23 +1,20 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Speaker { - public class Speaker - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? Name { get; set; } + [StringLength(200)] + public required string Name { get; init; } - [StringLength(4000)] - public string? Bio { get; set; } + [StringLength(4000)] + public string? Bio { get; init; } - [StringLength(1000)] - public string? WebSite { get; set; } + [StringLength(1000)] + public string? Website { get; init; } - public ICollection SessionSpeakers { get; set; } = - new List(); - } - } \ No newline at end of file + public ICollection SessionSpeakers { get; init; } = + new List(); +} diff --git a/code/session-5/GraphQL/Data/Track.cs b/code/session-5/GraphQL/Data/Track.cs index f2392b6..51bd27b 100644 --- a/code/session-5/GraphQL/Data/Track.cs +++ b/code/session-5/GraphQL/Data/Track.cs @@ -1,17 +1,14 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Track { - public class Track - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? Name { get; set; } + [StringLength(200)] + public required string Name { get; set; } - public ICollection Sessions { get; set; } = - new List(); - } -} \ No newline at end of file + public ICollection Sessions { get; init; } = + new List(); +} diff --git a/code/session-5/GraphQL/DataLoader/AttendeeByIdDataLoader.cs b/code/session-5/GraphQL/DataLoader/AttendeeByIdDataLoader.cs deleted file mode 100644 index a2bdbd0..0000000 --- a/code/session-5/GraphQL/DataLoader/AttendeeByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class AttendeeByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public AttendeeByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Attendees - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/DataLoader/SessionByIdDataLoader.cs b/code/session-5/GraphQL/DataLoader/SessionByIdDataLoader.cs deleted file mode 100644 index dbd675b..0000000 --- a/code/session-5/GraphQL/DataLoader/SessionByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class SessionByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public SessionByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Sessions - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/DataLoader/SpeakerByIdDataLoader.cs b/code/session-5/GraphQL/DataLoader/SpeakerByIdDataLoader.cs deleted file mode 100644 index 44d8208..0000000 --- a/code/session-5/GraphQL/DataLoader/SpeakerByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class SpeakerByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public SpeakerByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Speakers - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/DataLoader/TrackByIdDataLoader.cs b/code/session-5/GraphQL/DataLoader/TrackByIdDataLoader.cs deleted file mode 100644 index 4db1f95..0000000 --- a/code/session-5/GraphQL/DataLoader/TrackByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class TrackByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public TrackByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Tracks - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs b/code/session-5/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs index 370c767..49c13a8 100644 --- a/code/session-5/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs +++ b/code/session-5/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs @@ -1,32 +1,17 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using HotChocolate.Types; +namespace ConferencePlanner.GraphQL.Extensions; -namespace ConferencePlanner.GraphQL +public static class ObjectFieldDescriptorExtensions { - public static class ObjectFieldDescriptorExtensions + public static IObjectFieldDescriptor UseUpperCase(this IObjectFieldDescriptor descriptor) { - public static IObjectFieldDescriptor UseDbContext( - this IObjectFieldDescriptor descriptor) - where TDbContext : DbContext + return descriptor.Use(next => async context => { - return descriptor.UseScopedService( - create: s => s.GetRequiredService>().CreateDbContext(), - disposeAsync: (s, c) => c.DisposeAsync()); - } + await next(context); - public static IObjectFieldDescriptor UseUpperCase( - this IObjectFieldDescriptor descriptor) - { - return descriptor.Use(next => async context => + if (context.Result is string s) { - await next(context); - - if (context.Result is string s) - { - context.Result = s.ToUpperInvariant(); - } - }); - } + context.Result = s.ToUpperInvariant(); + } + }); } -} \ No newline at end of file +} diff --git a/code/session-5/GraphQL/Extensions/UseApplicationDbContextAttribute.cs b/code/session-5/GraphQL/Extensions/UseApplicationDbContextAttribute.cs deleted file mode 100644 index 79c9907..0000000 --- a/code/session-5/GraphQL/Extensions/UseApplicationDbContextAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Reflection; -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types; -using HotChocolate.Types.Descriptors; - -namespace ConferencePlanner.GraphQL -{ - public class UseApplicationDbContextAttribute : ObjectFieldDescriptorAttribute - { - public override void OnConfigure( - IDescriptorContext context, - IObjectFieldDescriptor descriptor, - MemberInfo member) - { - descriptor.UseDbContext(); - } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/Extensions/UseUpperCaseAttribute.cs b/code/session-5/GraphQL/Extensions/UseUpperCaseAttribute.cs index 8376b5b..b85152d 100644 --- a/code/session-5/GraphQL/Extensions/UseUpperCaseAttribute.cs +++ b/code/session-5/GraphQL/Extensions/UseUpperCaseAttribute.cs @@ -1,17 +1,15 @@ -using HotChocolate.Types; -using HotChocolate.Types.Descriptors; using System.Reflection; +using HotChocolate.Types.Descriptors; + +namespace ConferencePlanner.GraphQL.Extensions; -namespace ConferencePlanner.GraphQL +public sealed class UseUpperCaseAttribute : ObjectFieldDescriptorAttribute { - public class UseUpperCaseAttribute : ObjectFieldDescriptorAttribute + protected override void OnConfigure( + IDescriptorContext context, + IObjectFieldDescriptor descriptor, + MemberInfo member) { - public override void OnConfigure( - IDescriptorContext context, - IObjectFieldDescriptor descriptor, - MemberInfo member) - { - descriptor.UseUpperCase(); - } + descriptor.UseUpperCase(); } -} \ No newline at end of file +} diff --git a/code/session-5/GraphQL/GraphQL.csproj b/code/session-5/GraphQL/GraphQL.csproj index a717e60..428286d 100644 --- a/code/session-5/GraphQL/GraphQL.csproj +++ b/code/session-5/GraphQL/GraphQL.csproj @@ -1,18 +1,24 @@ - - - - net5.0 - ConferencePlanner.GraphQL - enable - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - + + + + net8.0 + enable + enable + ConferencePlanner.GraphQL + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/code/session-5/GraphQL/Migrations/20201010183502_Initial.Designer.cs b/code/session-5/GraphQL/Migrations/20201010183502_Initial.Designer.cs deleted file mode 100644 index dc170c1..0000000 --- a/code/session-5/GraphQL/Migrations/20201010183502_Initial.Designer.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -using ConferencePlanner.GraphQL.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace GraphQL.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20201010183502_Initial")] - partial class Initial - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bio") - .HasMaxLength(4000) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("WebSite") - .HasMaxLength(1000) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Speakers"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/code/session-5/GraphQL/Migrations/20201010183502_Initial.cs b/code/session-5/GraphQL/Migrations/20201010183502_Initial.cs deleted file mode 100644 index 69e30fb..0000000 --- a/code/session-5/GraphQL/Migrations/20201010183502_Initial.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace GraphQL.Migrations -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Speakers", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), - Bio = table.Column(type: "TEXT", maxLength: 4000, nullable: true), - WebSite = table.Column(type: "TEXT", maxLength: 1000, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Speakers", x => x.Id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Speakers"); - } - } -} diff --git a/code/session-5/GraphQL/Migrations/20240807140835_Initial.Designer.cs b/code/session-5/GraphQL/Migrations/20240807140835_Initial.Designer.cs new file mode 100644 index 0000000..d508064 --- /dev/null +++ b/code/session-5/GraphQL/Migrations/20240807140835_Initial.Designer.cs @@ -0,0 +1,55 @@ +// +using ConferencePlanner.GraphQL.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240807140835_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Bio") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Website") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.HasKey("Id"); + + b.ToTable("Speakers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/code/session-5/GraphQL/Migrations/20240807140835_Initial.cs b/code/session-5/GraphQL/Migrations/20240807140835_Initial.cs new file mode 100644 index 0000000..7301c7a --- /dev/null +++ b/code/session-5/GraphQL/Migrations/20240807140835_Initial.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Speakers", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Bio = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + Website = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Speakers", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Speakers"); + } + } +} diff --git a/code/session-5/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs b/code/session-5/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs similarity index 74% rename from code/session-5/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs rename to code/session-5/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs index 2e3d723..75c788f 100644 --- a/code/session-5/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs +++ b/code/session-5/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs @@ -5,47 +5,56 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20201010202211_Refactoring")] + [Migration("20240812080119_Refactoring")] partial class Refactoring { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("EmailAddress") .HasMaxLength(256) - .HasColumnType("TEXT"); + .HasColumnType("character varying(256)"); b.Property("FirstName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("LastName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("UserName") + b.Property("Username") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); - b.HasIndex("UserName") + b.HasIndex("Username") .IsUnique(); b.ToTable("Attendees"); @@ -55,25 +64,27 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Abstract") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("EndTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("StartTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("Title") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("TrackId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("Id"); @@ -85,10 +96,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("AttendeeId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "AttendeeId"); @@ -100,10 +111,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("SpeakerId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "SpeakerId"); @@ -116,20 +127,22 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Bio") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("WebSite") + b.Property("Website") .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(1000)"); b.HasKey("Id"); @@ -140,12 +153,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); diff --git a/code/session-4/GraphQL/Migrations/20201010202211_Refactoring.cs b/code/session-5/GraphQL/Migrations/20240812080119_Refactoring.cs similarity index 68% rename from code/session-4/GraphQL/Migrations/20201010202211_Refactoring.cs rename to code/session-5/GraphQL/Migrations/20240812080119_Refactoring.cs index ffdcfeb..e544f2e 100644 --- a/code/session-4/GraphQL/Migrations/20201010202211_Refactoring.cs +++ b/code/session-5/GraphQL/Migrations/20240812080119_Refactoring.cs @@ -1,22 +1,27 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { + /// public partial class Refactoring : Migration { + /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "Attendees", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - FirstName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - LastName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - UserName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - EmailAddress = table.Column(type: "TEXT", maxLength: 256, nullable: true) + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + FirstName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + LastName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Username = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + EmailAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true) }, constraints: table => { @@ -27,9 +32,9 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Tracks", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", maxLength: 200, nullable: false) + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false) }, constraints: table => { @@ -40,13 +45,13 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Sessions", columns: table => new { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Title = table.Column(type: "TEXT", maxLength: 200, nullable: false), - Abstract = table.Column(type: "TEXT", maxLength: 4000, nullable: true), - StartTime = table.Column(type: "TEXT", nullable: true), - EndTime = table.Column(type: "TEXT", nullable: true), - TrackId = table.Column(type: "INTEGER", nullable: true) + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Abstract = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + StartTime = table.Column(type: "timestamp with time zone", nullable: true), + EndTime = table.Column(type: "timestamp with time zone", nullable: true), + TrackId = table.Column(type: "integer", nullable: true) }, constraints: table => { @@ -55,16 +60,15 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "FK_Sessions_Tracks_TrackId", column: x => x.TrackId, principalTable: "Tracks", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); + principalColumn: "Id"); }); migrationBuilder.CreateTable( name: "SessionAttendee", columns: table => new { - SessionId = table.Column(type: "INTEGER", nullable: false), - AttendeeId = table.Column(type: "INTEGER", nullable: false) + SessionId = table.Column(type: "integer", nullable: false), + AttendeeId = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -87,8 +91,8 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "SessionSpeaker", columns: table => new { - SessionId = table.Column(type: "INTEGER", nullable: false), - SpeakerId = table.Column(type: "INTEGER", nullable: false) + SessionId = table.Column(type: "integer", nullable: false), + SpeakerId = table.Column(type: "integer", nullable: false) }, constraints: table => { @@ -108,9 +112,9 @@ protected override void Up(MigrationBuilder migrationBuilder) }); migrationBuilder.CreateIndex( - name: "IX_Attendees_UserName", + name: "IX_Attendees_Username", table: "Attendees", - column: "UserName", + column: "Username", unique: true); migrationBuilder.CreateIndex( @@ -129,6 +133,7 @@ protected override void Up(MigrationBuilder migrationBuilder) column: "SpeakerId"); } + /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( diff --git a/code/session-5/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs b/code/session-5/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs index a66dfe1..6cc21a5 100644 --- a/code/session-5/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/code/session-5/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs @@ -4,8 +4,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { [DbContext(typeof(ApplicationDbContext))] partial class ApplicationDbContextModelSnapshot : ModelSnapshot @@ -14,36 +17,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("EmailAddress") .HasMaxLength(256) - .HasColumnType("TEXT"); + .HasColumnType("character varying(256)"); b.Property("FirstName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("LastName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("UserName") + b.Property("Username") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); - b.HasIndex("UserName") + b.HasIndex("Username") .IsUnique(); b.ToTable("Attendees"); @@ -53,25 +61,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Abstract") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("EndTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("StartTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("Title") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("TrackId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("Id"); @@ -83,10 +93,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("AttendeeId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "AttendeeId"); @@ -98,10 +108,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("SpeakerId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "SpeakerId"); @@ -114,20 +124,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Bio") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("WebSite") + b.Property("Website") .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(1000)"); b.HasKey("Id"); @@ -138,12 +150,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); diff --git a/code/session-5/GraphQL/Program.cs b/code/session-5/GraphQL/Program.cs index c4914c6..e5e5f1d 100644 --- a/code/session-5/GraphQL/Program.cs +++ b/code/session-5/GraphQL/Program.cs @@ -1,26 +1,22 @@ -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 ConferencePlanner.GraphQL -{ - 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(); - }); - } -} +using ConferencePlanner.GraphQL.Data; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddDbContext( + options => options.UseNpgsql("Host=127.0.0.1;Username=graphql_workshop;Password=secret")) + .AddGraphQLServer() + .AddGlobalObjectIdentification() + .AddMutationConventions() + .AddDbContextCursorPagingProvider() + .AddPagingArguments() + .AddFiltering() + .AddSorting() + .AddGraphQLTypes(); + +var app = builder.Build(); + +app.MapGraphQL(); + +await app.RunWithGraphQLCommandsAsync(args); diff --git a/code/session-5/GraphQL/Properties/launchSettings.json b/code/session-5/GraphQL/Properties/launchSettings.json new file mode 100644 index 0000000..c0d9484 --- /dev/null +++ b/code/session-5/GraphQL/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7000;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/code/session-5/GraphQL/Sessions/AddSessionInput.cs b/code/session-5/GraphQL/Sessions/AddSessionInput.cs index db5995f..3474bf3 100644 --- a/code/session-5/GraphQL/Sessions/AddSessionInput.cs +++ b/code/session-5/GraphQL/Sessions/AddSessionInput.cs @@ -1,12 +1,8 @@ -using System.Collections.Generic; using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; -namespace ConferencePlanner.GraphQL.Sessions -{ - public record AddSessionInput( - string Title, - string? Abstract, - [ID(nameof(Speaker))] - IReadOnlyList SpeakerIds); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed record AddSessionInput( + string Title, + string? Abstract, + [property: ID] IReadOnlyList SpeakerIds); diff --git a/code/session-5/GraphQL/Sessions/AddSessionPayload.cs b/code/session-5/GraphQL/Sessions/AddSessionPayload.cs deleted file mode 100644 index 82775f8..0000000 --- a/code/session-5/GraphQL/Sessions/AddSessionPayload.cs +++ /dev/null @@ -1,20 +0,0 @@ -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class AddSessionPayload : Payload - { - public AddSessionPayload(Session session) - { - Session = session; - } - - public AddSessionPayload(UserError error) - : base(new[] { error }) - { - } - - public Session? Session { get; init; } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/Sessions/ScheduleSessionInput.cs b/code/session-5/GraphQL/Sessions/ScheduleSessionInput.cs index fc43463..9c4fd11 100644 --- a/code/session-5/GraphQL/Sessions/ScheduleSessionInput.cs +++ b/code/session-5/GraphQL/Sessions/ScheduleSessionInput.cs @@ -1,14 +1,9 @@ -using System; using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; -namespace ConferencePlanner.GraphQL.Sessions -{ - public record ScheduleSessionInput( - [ID(nameof(Session))] - int SessionId, - [ID(nameof(Track))] - int TrackId, - DateTimeOffset StartTime, - DateTimeOffset EndTime); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed record ScheduleSessionInput( + [property: ID] int SessionId, + [property: ID] int TrackId, + DateTimeOffset StartTime, + DateTimeOffset EndTime); diff --git a/code/session-5/GraphQL/Sessions/ScheduleSessionPayload.cs b/code/session-5/GraphQL/Sessions/ScheduleSessionPayload.cs deleted file mode 100644 index ce79df5..0000000 --- a/code/session-5/GraphQL/Sessions/ScheduleSessionPayload.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class ScheduleSessionPayload : SessionPayloadBase - { - public ScheduleSessionPayload(Session session) - : base(session) - { - } - - public ScheduleSessionPayload(UserError error) - : base(new[] { error }) - { - } - - public async Task GetTrackAsync( - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) - { - if (Session is null) - { - return null; - } - - return await trackById.LoadAsync(Session.Id, cancellationToken); - } - - [UseApplicationDbContext] - public async Task?> GetSpeakersAsync( - [ScopedService] ApplicationDbContext dbContext, - SpeakerByIdDataLoader speakerById, - CancellationToken cancellationToken) - { - if (Session is null) - { - return null; - } - - int[] speakerIds = await dbContext.Sessions - .Where(s => s.Id == Session.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SpeakerId)) - .ToArrayAsync(); - - return await speakerById.LoadAsync(speakerIds, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/Sessions/SessionDataLoaders.cs b/code/session-5/GraphQL/Sessions/SessionDataLoaders.cs new file mode 100644 index 0000000..738ce5e --- /dev/null +++ b/code/session-5/GraphQL/Sessions/SessionDataLoaders.cs @@ -0,0 +1,50 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Sessions; + +public static class SessionDataLoaders +{ + [DataLoader] + public static async Task> SessionByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => ids.Contains(s.Id)) + .Select(s => s.Id, selector) + .ToDictionaryAsync(s => s.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SpeakersBySessionIdAsync( + IReadOnlyList sessionIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => sessionIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionSpeakers.Select(ss => ss.Speaker), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } + + [DataLoader] + public static async Task> AttendeesBySessionIdAsync( + IReadOnlyList sessionIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => sessionIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionAttendees.Select(sa => sa.Attendee), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-5/GraphQL/Sessions/SessionExceptions.cs b/code/session-5/GraphQL/Sessions/SessionExceptions.cs new file mode 100644 index 0000000..fea5d77 --- /dev/null +++ b/code/session-5/GraphQL/Sessions/SessionExceptions.cs @@ -0,0 +1,9 @@ +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed class EndTimeInvalidException() : Exception("EndTime must be after StartTime."); + +public sealed class NoSpeakerException() : Exception("No speaker assigned."); + +public sealed class SessionNotFoundException() : Exception("Session not found."); + +public sealed class TitleEmptyException() : Exception("The title cannot be empty."); diff --git a/code/session-5/GraphQL/Sessions/SessionFilterInputType.cs b/code/session-5/GraphQL/Sessions/SessionFilterInputType.cs new file mode 100644 index 0000000..b6096b6 --- /dev/null +++ b/code/session-5/GraphQL/Sessions/SessionFilterInputType.cs @@ -0,0 +1,17 @@ +using ConferencePlanner.GraphQL.Data; +using HotChocolate.Data.Filters; + +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed class SessionFilterInputType : FilterInputType +{ + protected override void Configure(IFilterInputTypeDescriptor descriptor) + { + descriptor.BindFieldsExplicitly(); + + descriptor.Field(s => s.Title); + descriptor.Field(s => s.Abstract); + descriptor.Field(s => s.StartTime); + descriptor.Field(s => s.EndTime); + } +} diff --git a/code/session-5/GraphQL/Sessions/SessionMutations.cs b/code/session-5/GraphQL/Sessions/SessionMutations.cs index 74ca479..d220f3f 100644 --- a/code/session-5/GraphQL/Sessions/SessionMutations.cs +++ b/code/session-5/GraphQL/Sessions/SessionMutations.cs @@ -1,80 +1,73 @@ -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Common; using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; -namespace ConferencePlanner.GraphQL.Sessions +namespace ConferencePlanner.GraphQL.Sessions; + +[MutationType] +public static class SessionMutations { - [ExtendObjectType(Name = "Mutation")] - public class SessionMutations + [Error] + [Error] + public static async Task AddSessionAsync( + AddSessionInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - [UseApplicationDbContext] - public async Task AddSessionAsync( - AddSessionInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) + if (string.IsNullOrEmpty(input.Title)) { - if (string.IsNullOrEmpty(input.Title)) - { - return new AddSessionPayload( - new UserError("The title cannot be empty.", "TITLE_EMPTY")); - } + throw new TitleEmptyException(); + } - if (input.SpeakerIds.Count == 0) - { - return new AddSessionPayload( - new UserError("No speaker assigned.", "NO_SPEAKER")); - } + if (input.SpeakerIds.Count == 0) + { + throw new NoSpeakerException(); + } - var session = new Session - { - Title = input.Title, - Abstract = input.Abstract, - }; + var session = new Session + { + Title = input.Title, + Abstract = input.Abstract + }; - foreach (int speakerId in input.SpeakerIds) + foreach (var speakerId in input.SpeakerIds) + { + session.SessionSpeakers.Add(new SessionSpeaker { - session.SessionSpeakers.Add(new SessionSpeaker - { - SpeakerId = speakerId - }); - } + SpeakerId = speakerId + }); + } - context.Sessions.Add(session); - await context.SaveChangesAsync(cancellationToken); + dbContext.Sessions.Add(session); - return new AddSessionPayload(session); - } + await dbContext.SaveChangesAsync(cancellationToken); + + return session; + } - [UseApplicationDbContext] - public async Task ScheduleSessionAsync( - ScheduleSessionInput input, - [ScopedService] ApplicationDbContext context) + [Error] + [Error] + public static async Task ScheduleSessionAsync( + ScheduleSessionInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + if (input.EndTime < input.StartTime) { - if (input.EndTime < input.StartTime) - { - return new ScheduleSessionPayload( - new UserError("endTime has to be larger than startTime.", "END_TIME_INVALID")); - } + throw new EndTimeInvalidException(); + } - Session session = await context.Sessions.FindAsync(input.SessionId); - int? initialTrackId = session.TrackId; + var session = await dbContext.Sessions.FindAsync([input.SessionId], cancellationToken); - if (session is null) - { - return new ScheduleSessionPayload( - new UserError("Session not found.", "SESSION_NOT_FOUND")); - } + if (session is null) + { + throw new SessionNotFoundException(); + } - session.TrackId = input.TrackId; - session.StartTime = input.StartTime; - session.EndTime = input.EndTime; + session.TrackId = input.TrackId; + session.StartTime = input.StartTime; + session.EndTime = input.EndTime; - await context.SaveChangesAsync(); + await dbContext.SaveChangesAsync(cancellationToken); - return new ScheduleSessionPayload(session); - } + return session; } -} \ No newline at end of file +} diff --git a/code/session-5/GraphQL/Sessions/SessionPayloadBase.cs b/code/session-5/GraphQL/Sessions/SessionPayloadBase.cs deleted file mode 100644 index 888ad50..0000000 --- a/code/session-5/GraphQL/Sessions/SessionPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class SessionPayloadBase : Payload - { - protected SessionPayloadBase(Session session) - { - Session = session; - } - - protected SessionPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Session? Session { get; } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/Sessions/SessionQueries.cs b/code/session-5/GraphQL/Sessions/SessionQueries.cs index 9e8428d..7c1294c 100644 --- a/code/session-5/GraphQL/Sessions/SessionQueries.cs +++ b/code/session-5/GraphQL/Sessions/SessionQueries.cs @@ -1,34 +1,37 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; -namespace ConferencePlanner.GraphQL.Sessions +namespace ConferencePlanner.GraphQL.Sessions; + +[QueryType] +public static class SessionQueries { - [ExtendObjectType(Name = "Query")] - public class SessionQueries + [UsePaging] + [UseFiltering] + [UseSorting] + public static IQueryable GetSessions(ApplicationDbContext dbContext) { - [UseApplicationDbContext] - public async Task> GetSessionsAsync( - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - await context.Sessions.ToListAsync(cancellationToken); + return dbContext.Sessions.AsNoTracking().OrderBy(s => s.Title).ThenBy(s => s.Id); + } - public Task GetSessionByIdAsync( - [ID(nameof(Session))] int id, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - sessionById.LoadAsync(id, cancellationToken); + [NodeResolver] + public static async Task GetSessionByIdAsync( + int id, + ISessionByIdDataLoader sessionById, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionById.Select(selection).LoadAsync(id, cancellationToken); + } - public async Task> GetSessionsByIdAsync( - [ID(nameof(Session))] int[] ids, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - await sessionById.LoadAsync(ids, cancellationToken); + public static async Task> GetSessionsByIdAsync( + [ID] int[] ids, + ISessionByIdDataLoader sessionById, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionById.Select(selection).LoadRequiredAsync(ids, cancellationToken); } -} \ No newline at end of file +} diff --git a/code/session-5/GraphQL/Sessions/SessionType.cs b/code/session-5/GraphQL/Sessions/SessionType.cs new file mode 100644 index 0000000..abda805 --- /dev/null +++ b/code/session-5/GraphQL/Sessions/SessionType.cs @@ -0,0 +1,60 @@ +using ConferencePlanner.GraphQL.Data; +using ConferencePlanner.GraphQL.Tracks; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Sessions; + +[ObjectType] +public static partial class SessionType +{ + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field(s => s.TrackId) + .ID(); + } + + public static TimeSpan Duration([Parent("StartTime EndTime")] Session session) + => session.Duration; + + [BindMember(nameof(Session.SessionSpeakers))] + public static async Task> GetSpeakersAsync( + [Parent] Session session, + ISpeakersBySessionIdDataLoader speakersBySessionId, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakersBySessionId + .Select(selection) + .LoadRequiredAsync(session.Id, cancellationToken); + } + + [BindMember(nameof(Session.SessionAttendees))] + public static async Task> GetAttendeesAsync( + [Parent(nameof(Session.Id))] Session session, + IAttendeesBySessionIdDataLoader attendeesBySessionId, + ISelection selection, + CancellationToken cancellationToken) + { + return await attendeesBySessionId + .Select(selection) + .LoadRequiredAsync(session.Id, cancellationToken); + } + + public static async Task GetTrackAsync( + [Parent(nameof(Session.TrackId))] Session session, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + if (session.TrackId is null) + { + return null; + } + + return await trackById + .Select(selection) + .LoadAsync(session.TrackId.Value, cancellationToken); + } +} diff --git a/code/session-5/GraphQL/Speakers/AddSpeakerInput.cs b/code/session-5/GraphQL/Speakers/AddSpeakerInput.cs index a81f45f..bdc584a 100644 --- a/code/session-5/GraphQL/Speakers/AddSpeakerInput.cs +++ b/code/session-5/GraphQL/Speakers/AddSpeakerInput.cs @@ -1,7 +1,6 @@ -namespace ConferencePlanner.GraphQL.Speakers -{ - public record AddSpeakerInput( - string Name, - string? Bio, - string? WebSite); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Speakers; + +public sealed record AddSpeakerInput( + string Name, + string? Bio, + string? Website); diff --git a/code/session-5/GraphQL/Speakers/AddSpeakerPayload.cs b/code/session-5/GraphQL/Speakers/AddSpeakerPayload.cs deleted file mode 100644 index aaf0ab0..0000000 --- a/code/session-5/GraphQL/Speakers/AddSpeakerPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Speakers -{ - public class AddSpeakerPayload : SpeakerPayloadBase - { - public AddSpeakerPayload(Speaker speaker) - : base(speaker) - { - } - - public AddSpeakerPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/Speakers/SpeakerDataLoaders.cs b/code/session-5/GraphQL/Speakers/SpeakerDataLoaders.cs new file mode 100644 index 0000000..c2c748e --- /dev/null +++ b/code/session-5/GraphQL/Speakers/SpeakerDataLoaders.cs @@ -0,0 +1,36 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Speakers; + +public static class SpeakerDataLoaders +{ + [DataLoader] + public static async Task> SpeakerByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Speakers + .AsNoTracking() + .Where(s => ids.Contains(s.Id)) + .Select(s => s.Id, selector) + .ToDictionaryAsync(s => s.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SessionsBySpeakerIdAsync( + IReadOnlyList speakerIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Speakers + .AsNoTracking() + .Where(s => speakerIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionSpeakers.Select(ss => ss.Session), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-5/GraphQL/Speakers/SpeakerMutations.cs b/code/session-5/GraphQL/Speakers/SpeakerMutations.cs index 55fc6f5..0a8ad7a 100644 --- a/code/session-5/GraphQL/Speakers/SpeakerMutations.cs +++ b/code/session-5/GraphQL/Speakers/SpeakerMutations.cs @@ -1,29 +1,26 @@ -using System.Threading.Tasks; using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; -namespace ConferencePlanner.GraphQL.Speakers +namespace ConferencePlanner.GraphQL.Speakers; + +[MutationType] +public static class SpeakerMutations { - [ExtendObjectType(Name = "Mutation")] - public class SpeakerMutations + public static async Task AddSpeakerAsync( + AddSpeakerInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - [UseApplicationDbContext] - public async Task AddSpeakerAsync( - AddSpeakerInput input, - [ScopedService] ApplicationDbContext context) + var speaker = new Speaker { - var speaker = new Speaker - { - Name = input.Name, - Bio = input.Bio, - WebSite = input.WebSite - }; + Name = input.Name, + Bio = input.Bio, + Website = input.Website + }; + + dbContext.Speakers.Add(speaker); - context.Speakers.Add(speaker); - await context.SaveChangesAsync(); + await dbContext.SaveChangesAsync(cancellationToken); - return new AddSpeakerPayload(speaker); - } + return speaker; } -} \ No newline at end of file +} diff --git a/code/session-5/GraphQL/Speakers/SpeakerPayloadBase.cs b/code/session-5/GraphQL/Speakers/SpeakerPayloadBase.cs deleted file mode 100644 index a2077d7..0000000 --- a/code/session-5/GraphQL/Speakers/SpeakerPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Speakers -{ - public class SpeakerPayloadBase : Payload - { - protected SpeakerPayloadBase(Speaker speaker) - { - Speaker = speaker; - } - - protected SpeakerPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Speaker? Speaker { get; init; } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/Speakers/SpeakerQueries.cs b/code/session-5/GraphQL/Speakers/SpeakerQueries.cs index 69eb814..8283587 100644 --- a/code/session-5/GraphQL/Speakers/SpeakerQueries.cs +++ b/code/session-5/GraphQL/Speakers/SpeakerQueries.cs @@ -1,32 +1,35 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; -namespace ConferencePlanner.GraphQL.Speakers +namespace ConferencePlanner.GraphQL.Speakers; + +[QueryType] +public static class SpeakerQueries { - [ExtendObjectType(Name = "Query")] - public class SpeakerQueries + [UsePaging] + public static IQueryable GetSpeakers(ApplicationDbContext dbContext) { - [UseApplicationDbContext] - public Task> GetSpeakers([ScopedService] ApplicationDbContext context) => - context.Speakers.ToListAsync(); + return dbContext.Speakers.AsNoTracking().OrderBy(s => s.Name).ThenBy(s => s.Id); + } - public Task GetSpeakerByIdAsync( - [ID(nameof(Speaker))]int id, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - dataLoader.LoadAsync(id, cancellationToken); + [NodeResolver] + public static async Task GetSpeakerByIdAsync( + int id, + ISpeakerByIdDataLoader speakerById, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakerById.Select(selection).LoadAsync(id, cancellationToken); + } - public async Task> GetSpeakersByIdAsync( - [ID(nameof(Speaker))]int[] ids, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - await dataLoader.LoadAsync(ids, cancellationToken); + public static async Task> GetSpeakersByIdAsync( + [ID] int[] ids, + ISpeakerByIdDataLoader speakerById, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakerById.Select(selection).LoadRequiredAsync(ids, cancellationToken); } -} \ No newline at end of file +} diff --git a/code/session-5/GraphQL/Speakers/SpeakerType.cs b/code/session-5/GraphQL/Speakers/SpeakerType.cs new file mode 100644 index 0000000..54555fd --- /dev/null +++ b/code/session-5/GraphQL/Speakers/SpeakerType.cs @@ -0,0 +1,21 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Speakers; + +[ObjectType] +public static partial class SpeakerType +{ + [BindMember(nameof(Speaker.SessionSpeakers))] + public static async Task> GetSessionsAsync( + [Parent] Speaker speaker, + ISessionsBySpeakerIdDataLoader sessionsBySpeakerId, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsBySpeakerId + .Select(selection) + .LoadRequiredAsync(speaker.Id, cancellationToken); + } +} diff --git a/code/session-5/GraphQL/Startup.cs b/code/session-5/GraphQL/Startup.cs deleted file mode 100644 index 683d4e0..0000000 --- a/code/session-5/GraphQL/Startup.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using ConferencePlanner.GraphQL.Sessions; -using ConferencePlanner.GraphQL.Speakers; -using ConferencePlanner.GraphQL.Tracks; -using ConferencePlanner.GraphQL.Types; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace ConferencePlanner.GraphQL -{ - 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.AddPooledDbContextFactory( - options => options.UseSqlite("Data Source=conferences.db")); - - services - .AddGraphQLServer() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport() - .AddDataLoader() - .AddDataLoader(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGraphQL(); - }); - } - } -} diff --git a/code/session-5/GraphQL/Tracks/AddTrackInput.cs b/code/session-5/GraphQL/Tracks/AddTrackInput.cs index 5c83b34..1aaf313 100644 --- a/code/session-5/GraphQL/Tracks/AddTrackInput.cs +++ b/code/session-5/GraphQL/Tracks/AddTrackInput.cs @@ -1,4 +1,3 @@ -namespace ConferencePlanner.GraphQL.Tracks -{ - public record AddTrackInput(string Name); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Tracks; + +public sealed record AddTrackInput(string Name); diff --git a/code/session-5/GraphQL/Tracks/AddTrackPayload.cs b/code/session-5/GraphQL/Tracks/AddTrackPayload.cs deleted file mode 100644 index 8f35b13..0000000 --- a/code/session-5/GraphQL/Tracks/AddTrackPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class AddTrackPayload : TrackPayloadBase - { - public AddTrackPayload(Track track) - : base(track) - { - } - - public AddTrackPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/Tracks/RenameTrackInput.cs b/code/session-5/GraphQL/Tracks/RenameTrackInput.cs index 516c6a0..d11ad39 100644 --- a/code/session-5/GraphQL/Tracks/RenameTrackInput.cs +++ b/code/session-5/GraphQL/Tracks/RenameTrackInput.cs @@ -1,7 +1,5 @@ using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; -namespace ConferencePlanner.GraphQL.Tracks -{ - public record RenameTrackInput([ID(nameof(Track))] int Id, string Name); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Tracks; + +public sealed record RenameTrackInput([property: ID] int Id, string Name); diff --git a/code/session-5/GraphQL/Tracks/RenameTrackPayload.cs b/code/session-5/GraphQL/Tracks/RenameTrackPayload.cs deleted file mode 100644 index ca4c8a1..0000000 --- a/code/session-5/GraphQL/Tracks/RenameTrackPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class RenameTrackPayload : TrackPayloadBase - { - public RenameTrackPayload(Track track) - : base(track) - { - } - - public RenameTrackPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/Tracks/TrackDataLoaders.cs b/code/session-5/GraphQL/Tracks/TrackDataLoaders.cs new file mode 100644 index 0000000..f592b8a --- /dev/null +++ b/code/session-5/GraphQL/Tracks/TrackDataLoaders.cs @@ -0,0 +1,38 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Tracks; + +public static class TrackDataLoaders +{ + [DataLoader] + public static async Task> TrackByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Tracks + .AsNoTracking() + .Where(t => ids.Contains(t.Id)) + .Select(t => t.Id, selector) + .ToDictionaryAsync(t => t.Id, cancellationToken); + } + + [DataLoader] + public static async Task>> SessionsByTrackIdAsync( + IReadOnlyList trackIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + PagingArguments pagingArguments, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => s.TrackId != null && trackIds.Contains((int)s.TrackId)) + .OrderBy(s => s.Id) + .Select(s => s.TrackId, selector) + .ToBatchPageAsync(s => (int)s.TrackId!, pagingArguments, cancellationToken); + } +} diff --git a/code/session-5/GraphQL/Tracks/TrackExceptions.cs b/code/session-5/GraphQL/Tracks/TrackExceptions.cs new file mode 100644 index 0000000..8df488d --- /dev/null +++ b/code/session-5/GraphQL/Tracks/TrackExceptions.cs @@ -0,0 +1,3 @@ +namespace ConferencePlanner.GraphQL.Tracks; + +public sealed class TrackNotFoundException() : Exception("Track not found."); diff --git a/code/session-5/GraphQL/Tracks/TrackMutations.cs b/code/session-5/GraphQL/Tracks/TrackMutations.cs index 88727b9..a91671c 100644 --- a/code/session-5/GraphQL/Tracks/TrackMutations.cs +++ b/code/session-5/GraphQL/Tracks/TrackMutations.cs @@ -1,40 +1,41 @@ -using System.Threading; -using System.Threading.Tasks; using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; -namespace ConferencePlanner.GraphQL.Tracks +namespace ConferencePlanner.GraphQL.Tracks; + +[MutationType] +public static class TrackMutations { - [ExtendObjectType(Name = "Mutation")] - public class TrackMutations + public static async Task AddTrackAsync( + AddTrackInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - [UseApplicationDbContext] - public async Task AddTrackAsync( - AddTrackInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - var track = new Track { Name = input.Name }; - context.Tracks.Add(track); + var track = new Track { Name = input.Name }; - await context.SaveChangesAsync(cancellationToken); + dbContext.Tracks.Add(track); - return new AddTrackPayload(track); - } + await dbContext.SaveChangesAsync(cancellationToken); - [UseApplicationDbContext] - public async Task RenameTrackAsync( - RenameTrackInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - Track track = await context.Tracks.FindAsync(input.Id); - track.Name = input.Name; + return track; + } - await context.SaveChangesAsync(cancellationToken); + [Error] + public static async Task RenameTrackAsync( + RenameTrackInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var track = await dbContext.Tracks.FindAsync([input.Id], cancellationToken); - return new RenameTrackPayload(track); + if (track is null) + { + throw new TrackNotFoundException(); } + + track.Name = input.Name; + + await dbContext.SaveChangesAsync(cancellationToken); + + return track; } -} \ No newline at end of file +} diff --git a/code/session-5/GraphQL/Tracks/TrackPayloadBase.cs b/code/session-5/GraphQL/Tracks/TrackPayloadBase.cs deleted file mode 100644 index de11da7..0000000 --- a/code/session-5/GraphQL/Tracks/TrackPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class TrackPayloadBase : Payload - { - public TrackPayloadBase(Track track) - { - Track = track; - } - - public TrackPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Track? Track { get; } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/Tracks/TrackQueries.cs b/code/session-5/GraphQL/Tracks/TrackQueries.cs index dd0288f..948efac 100644 --- a/code/session-5/GraphQL/Tracks/TrackQueries.cs +++ b/code/session-5/GraphQL/Tracks/TrackQueries.cs @@ -1,49 +1,35 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Tracks; -namespace ConferencePlanner.GraphQL.Tracks +[QueryType] +public static class TrackQueries { - [ExtendObjectType(Name = "Query")] - public class TrackQueries + [UsePaging] + public static IQueryable GetTracks(ApplicationDbContext dbContext) { - [UseApplicationDbContext] - public async Task> GetTracksAsync( - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - await context.Tracks.ToListAsync(cancellationToken); - - [UseApplicationDbContext] - public Task GetTrackByNameAsync( - string name, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - context.Tracks.FirstAsync(t => t.Name == name); - - [UseApplicationDbContext] - public async Task> GetTrackByNamesAsync( - string[] names, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - await context.Tracks.Where(t => names.Contains(t.Name)).ToListAsync(); + return dbContext.Tracks.AsNoTracking().OrderBy(t => t.Name).ThenBy(t => t.Id); + } - public Task GetTrackByIdAsync( - [ID(nameof(Track))] int id, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) => - trackById.LoadAsync(id, cancellationToken); + [NodeResolver] + public static async Task GetTrackByIdAsync( + int id, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + return await trackById.Select(selection).LoadAsync(id, cancellationToken); + } - public async Task> GetTracksByIdAsync( - [ID(nameof(Track))] int[] ids, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) => - await trackById.LoadAsync(ids, cancellationToken); + public static async Task> GetTracksByIdAsync( + [ID] int[] ids, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + return await trackById.Select(selection).LoadRequiredAsync(ids, cancellationToken); } -} \ No newline at end of file +} diff --git a/code/session-5/GraphQL/Tracks/TrackType.cs b/code/session-5/GraphQL/Tracks/TrackType.cs new file mode 100644 index 0000000..2bc3559 --- /dev/null +++ b/code/session-5/GraphQL/Tracks/TrackType.cs @@ -0,0 +1,34 @@ +using ConferencePlanner.GraphQL.Data; +using ConferencePlanner.GraphQL.Extensions; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using HotChocolate.Types.Pagination; + +namespace ConferencePlanner.GraphQL.Tracks; + +[ObjectType] +public static partial class TrackType +{ + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field(t => t.Name) + .ParentRequires(nameof(Track.Name)) + .UseUpperCase(); + } + + [UsePaging] + public static async Task> GetSessionsAsync( + [Parent] Track track, + ISessionsByTrackIdDataLoader sessionsByTrackId, + PagingArguments pagingArguments, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsByTrackId + .With(pagingArguments) + .Select(selection) + .LoadAsync(track.Id, cancellationToken) + .ToConnectionAsync(); + } +} diff --git a/code/session-5/GraphQL/Types/AttendeeType.cs b/code/session-5/GraphQL/Types/AttendeeType.cs deleted file mode 100644 index 62274c7..0000000 --- a/code/session-5/GraphQL/Types/AttendeeType.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Types -{ - public class AttendeeType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionsAttendees) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("sessions"); - } - - private class AttendeeResolvers - { - public async Task> GetSessionsAsync( - Attendee attendee, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Attendees - .Where(a => a.Id == attendee.Id) - .Include(a => a.SessionsAttendees) - .SelectMany(a => a.SessionsAttendees.Select(t => t.SessionId)) - .ToArrayAsync(); - - return await sessionById.LoadAsync(speakerIds, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/Types/SessionType.cs b/code/session-5/GraphQL/Types/SessionType.cs deleted file mode 100644 index 8a558f7..0000000 --- a/code/session-5/GraphQL/Types/SessionType.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Types -{ - public class SessionType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionSpeakers) - .ResolveWith(t => t.GetSpeakersAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("speakers"); - - descriptor - .Field(t => t.SessionAttendees) - .ResolveWith(t => t.GetAttendeesAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("attendees"); - - descriptor - .Field(t => t.Track) - .ResolveWith(t => t.GetTrackAsync(default!, default!, default)); - - descriptor - .Field(t => t.TrackId) - .ID(nameof(Track)); - } - - private class SessionResolvers - { - public async Task> GetSpeakersAsync( - Session session, - [ScopedService] ApplicationDbContext dbContext, - SpeakerByIdDataLoader speakerById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Sessions - .Where(s => s.Id == session.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SpeakerId)) - .ToArrayAsync(); - - return await speakerById.LoadAsync(speakerIds, cancellationToken); - } - - public async Task> GetAttendeesAsync( - Session session, - [ScopedService] ApplicationDbContext dbContext, - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) - { - int[] attendeeIds = await dbContext.Sessions - .Where(s => s.Id == session.Id) - .Include(session => session.SessionAttendees) - .SelectMany(session => session.SessionAttendees.Select(t => t.AttendeeId)) - .ToArrayAsync(); - - return await attendeeById.LoadAsync(attendeeIds, cancellationToken); - } - - public async Task GetTrackAsync( - Session session, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) - { - if (session.TrackId is null) - { - return null; - } - - return await trackById.LoadAsync(session.TrackId.Value, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/Types/SpeakerType.cs b/code/session-5/GraphQL/Types/SpeakerType.cs deleted file mode 100644 index 5e2b2e7..0000000 --- a/code/session-5/GraphQL/Types/SpeakerType.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Types -{ - public class SpeakerType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionSpeakers) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("sessions"); - } - - private class SpeakerResolvers - { - public async Task> GetSessionsAsync( - Speaker speaker, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Speakers - .Where(s => s.Id == speaker.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SessionId)) - .ToArrayAsync(); - - return await sessionById.LoadAsync(speakerIds, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/Types/TrackType.cs b/code/session-5/GraphQL/Types/TrackType.cs deleted file mode 100644 index 4cfd481..0000000 --- a/code/session-5/GraphQL/Types/TrackType.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Types -{ - public class TrackType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => - ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.Sessions) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .UsePaging>() - .Name("sessions"); - - descriptor - .Field(t => t.Name) - .UseUpperCase(); - } - - private class TrackResolvers - { - public async Task> GetSessionsAsync( - Track track, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] sessionIds = await dbContext.Sessions - .Where(s => s.Id == track.Id) - .Select(s => s.Id) - .ToArrayAsync(); - - return await sessionById.LoadAsync(sessionIds, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-5/GraphQL/appsettings.Development.json b/code/session-5/GraphQL/appsettings.Development.json index dba68eb..0c208ae 100644 --- a/code/session-5/GraphQL/appsettings.Development.json +++ b/code/session-5/GraphQL/appsettings.Development.json @@ -1,9 +1,8 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Information", + "Microsoft.AspNetCore": "Warning" } } } diff --git a/code/session-5/GraphQL/appsettings.json b/code/session-5/GraphQL/appsettings.json index 81ff877..10f68b8 100644 --- a/code/session-5/GraphQL/appsettings.json +++ b/code/session-5/GraphQL/appsettings.json @@ -1,10 +1,9 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/code/session-5/docker-compose.yml b/code/session-5/docker-compose.yml new file mode 100644 index 0000000..728458d --- /dev/null +++ b/code/session-5/docker-compose.yml @@ -0,0 +1,23 @@ +name: graphql-workshop + +services: + graphql-workshop-postgres: + container_name: graphql-workshop-postgres + image: postgres:17.2 + environment: + POSTGRES_USER: graphql_workshop + POSTGRES_PASSWORD: secret + POSTGRES_DB: graphql_workshop + networks: [graphql-workshop] + ports: ["5432:5432"] + volumes: + - type: volume + source: postgres-data + target: /var/lib/postgresql/data + +networks: + graphql-workshop: + name: graphql-workshop + +volumes: + postgres-data: diff --git a/code/session-6/.config/dotnet-tools.json b/code/session-6/.config/dotnet-tools.json index c735fef..aad8137 100644 --- a/code/session-6/.config/dotnet-tools.json +++ b/code/session-6/.config/dotnet-tools.json @@ -3,10 +3,11 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "5.0.0", + "version": "9.0.1", "commands": [ "dotnet-ef" - ] + ], + "rollForward": false } } -} \ No newline at end of file +} diff --git a/code/session-6/.vscode/launch.json b/code/session-6/.vscode/launch.json deleted file mode 100644 index e90375e..0000000 --- a/code/session-6/.vscode/launch.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - // 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}/GraphQL/bin/Debug/net5.0/GraphQL.dll", - "args": [], - "cwd": "${workspaceFolder}/GraphQL", - "stopAtEntry": false, - "serverReadyAction": { - "action": "openExternally", - "pattern": "\\bNow 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/code/session-6/.vscode/tasks.json b/code/session-6/.vscode/tasks.json deleted file mode 100644 index 31c32bd..0000000 --- a/code/session-6/.vscode/tasks.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - // 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/code/session-6/ConferencePlanner.sln b/code/session-6/ConferencePlanner.sln index 42fadb8..8c414fc 100644 --- a/code/session-6/ConferencePlanner.sln +++ b/code/session-6/ConferencePlanner.sln @@ -1,34 +1,22 @@ - -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}") = "GraphQL", "GraphQL\GraphQL.csproj", "{48385280-56F1-4937-9655-E6A79184740B}" -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 - {48385280-56F1-4937-9655-E6A79184740B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x64.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x64.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x86.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x86.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|Any CPU.Build.0 = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x64.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x64.Build.0 = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x86.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL", "GraphQL\GraphQL.csproj", "{D96823B9-86D3-4D54-A803-F1D43AEBE1FD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/code/session-6/GraphQL/Attendees/AttendeeDataLoaders.cs b/code/session-6/GraphQL/Attendees/AttendeeDataLoaders.cs new file mode 100644 index 0000000..56b8e21 --- /dev/null +++ b/code/session-6/GraphQL/Attendees/AttendeeDataLoaders.cs @@ -0,0 +1,36 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Attendees; + +public static class AttendeeDataLoaders +{ + [DataLoader] + public static async Task> AttendeeByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Attendees + .AsNoTracking() + .Where(a => ids.Contains(a.Id)) + .Select(a => a.Id, selector) + .ToDictionaryAsync(a => a.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SessionsByAttendeeIdAsync( + IReadOnlyList attendeeIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Attendees + .AsNoTracking() + .Where(a => attendeeIds.Contains(a.Id)) + .Select(a => a.Id, a => a.SessionsAttendees.Select(sa => sa.Session), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-6/GraphQL/Attendees/AttendeeExceptions.cs b/code/session-6/GraphQL/Attendees/AttendeeExceptions.cs new file mode 100644 index 0000000..59fe765 --- /dev/null +++ b/code/session-6/GraphQL/Attendees/AttendeeExceptions.cs @@ -0,0 +1,3 @@ +namespace ConferencePlanner.GraphQL.Attendees; + +public sealed class AttendeeNotFoundException() : Exception("Attendee not found."); diff --git a/code/session-6/GraphQL/Attendees/AttendeeMutations.cs b/code/session-6/GraphQL/Attendees/AttendeeMutations.cs new file mode 100644 index 0000000..6496fed --- /dev/null +++ b/code/session-6/GraphQL/Attendees/AttendeeMutations.cs @@ -0,0 +1,56 @@ +using ConferencePlanner.GraphQL.Data; +using HotChocolate.Subscriptions; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Attendees; + +[MutationType] +public static class AttendeeMutations +{ + public static async Task RegisterAttendeeAsync( + RegisterAttendeeInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var attendee = new Attendee + { + FirstName = input.FirstName, + LastName = input.LastName, + Username = input.Username, + EmailAddress = input.EmailAddress + }; + + dbContext.Attendees.Add(attendee); + + await dbContext.SaveChangesAsync(cancellationToken); + + return attendee; + } + + public static async Task CheckInAttendeeAsync( + CheckInAttendeeInput input, + ApplicationDbContext dbContext, + ITopicEventSender eventSender, + CancellationToken cancellationToken) + { + var attendee = await dbContext.Attendees.FirstOrDefaultAsync( + a => a.Id == input.AttendeeId, + cancellationToken); + + if (attendee is null) + { + throw new AttendeeNotFoundException(); + } + + attendee.SessionsAttendees.Add(new SessionAttendee { SessionId = input.SessionId }); + + await dbContext.SaveChangesAsync(cancellationToken); + + await eventSender.SendAsync( + $"OnAttendeeCheckedIn_{input.SessionId}", + input.AttendeeId, + cancellationToken); + + return attendee; + } +} diff --git a/code/session-6/GraphQL/Attendees/AttendeeQueries.cs b/code/session-6/GraphQL/Attendees/AttendeeQueries.cs new file mode 100644 index 0000000..bda229c --- /dev/null +++ b/code/session-6/GraphQL/Attendees/AttendeeQueries.cs @@ -0,0 +1,35 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Attendees; + +[QueryType] +public static class AttendeeQueries +{ + [UsePaging] + public static IQueryable GetAttendees(ApplicationDbContext dbContext) + { + return dbContext.Attendees.AsNoTracking().OrderBy(a => a.Username); + } + + [NodeResolver] + public static async Task GetAttendeeByIdAsync( + int id, + IAttendeeByIdDataLoader attendeeById, + ISelection selection, + CancellationToken cancellationToken) + { + return await attendeeById.Select(selection).LoadAsync(id, cancellationToken); + } + + public static async Task> GetAttendeesByIdAsync( + [ID] int[] ids, + IAttendeeByIdDataLoader attendeeById, + ISelection selection, + CancellationToken cancellationToken) + { + return await attendeeById.Select(selection).LoadRequiredAsync(ids, cancellationToken); + } +} diff --git a/code/session-6/GraphQL/Attendees/AttendeeSubscriptions.cs b/code/session-6/GraphQL/Attendees/AttendeeSubscriptions.cs new file mode 100644 index 0000000..5d35833 --- /dev/null +++ b/code/session-6/GraphQL/Attendees/AttendeeSubscriptions.cs @@ -0,0 +1,27 @@ +using ConferencePlanner.GraphQL.Data; +using HotChocolate.Execution; +using HotChocolate.Subscriptions; + +namespace ConferencePlanner.GraphQL.Attendees; + +[SubscriptionType] +public static class AttendeeSubscriptions +{ + [Subscribe(With = nameof(SubscribeToOnAttendeeCheckedInAsync))] + public static SessionAttendeeCheckIn OnAttendeeCheckedIn( + [ID] int sessionId, + [EventMessage] int attendeeId) + { + return new SessionAttendeeCheckIn(attendeeId, sessionId); + } + + public static async ValueTask> SubscribeToOnAttendeeCheckedInAsync( + int sessionId, + ITopicEventReceiver eventReceiver, + CancellationToken cancellationToken) + { + return await eventReceiver.SubscribeAsync( + $"OnAttendeeCheckedIn_{sessionId}", + cancellationToken); + } +} diff --git a/code/session-6/GraphQL/Attendees/AttendeeType.cs b/code/session-6/GraphQL/Attendees/AttendeeType.cs new file mode 100644 index 0000000..a76e76a --- /dev/null +++ b/code/session-6/GraphQL/Attendees/AttendeeType.cs @@ -0,0 +1,32 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Attendees; + +[ObjectType] +public static partial class AttendeeType +{ + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .ImplementsNode() + .IdField(a => a.Id) + .ResolveNode( + async (ctx, id) + => await ctx.DataLoader() + .LoadAsync(id, ctx.RequestAborted)); + } + + [BindMember(nameof(Attendee.SessionsAttendees))] + public static async Task> GetSessionsAsync( + [Parent] Attendee attendee, + ISessionsByAttendeeIdDataLoader sessionsByAttendeeId, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsByAttendeeId + .Select(selection) + .LoadRequiredAsync(attendee.Id, cancellationToken); + } +} diff --git a/code/session-6/GraphQL/Attendees/CheckInAttendeeInput.cs b/code/session-6/GraphQL/Attendees/CheckInAttendeeInput.cs new file mode 100644 index 0000000..8f52b73 --- /dev/null +++ b/code/session-6/GraphQL/Attendees/CheckInAttendeeInput.cs @@ -0,0 +1,7 @@ +using ConferencePlanner.GraphQL.Data; + +namespace ConferencePlanner.GraphQL.Attendees; + +public sealed record CheckInAttendeeInput( + [property: ID] int SessionId, + [property: ID] int AttendeeId); diff --git a/code/session-6/GraphQL/Attendees/RegisterAttendeeInput.cs b/code/session-6/GraphQL/Attendees/RegisterAttendeeInput.cs new file mode 100644 index 0000000..4b40751 --- /dev/null +++ b/code/session-6/GraphQL/Attendees/RegisterAttendeeInput.cs @@ -0,0 +1,7 @@ +namespace ConferencePlanner.GraphQL.Attendees; + +public sealed record RegisterAttendeeInput( + string FirstName, + string LastName, + string Username, + string EmailAddress); diff --git a/code/session-6/GraphQL/Attendees/SessionAttendeeCheckIn.cs b/code/session-6/GraphQL/Attendees/SessionAttendeeCheckIn.cs new file mode 100644 index 0000000..fa6bacf --- /dev/null +++ b/code/session-6/GraphQL/Attendees/SessionAttendeeCheckIn.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore; +using ConferencePlanner.GraphQL.Data; +using ConferencePlanner.GraphQL.Sessions; + +namespace ConferencePlanner.GraphQL.Attendees; + +public sealed class SessionAttendeeCheckIn(int attendeeId, int sessionId) +{ + [ID] + public int AttendeeId { get; } = attendeeId; + + [ID] + public int SessionId { get; } = sessionId; + + public async Task CheckInCountAsync( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => s.Id == SessionId) + .SelectMany(s => s.SessionAttendees) + .CountAsync(cancellationToken); + } + + public async Task GetAttendeeAsync( + IAttendeeByIdDataLoader attendeeById, + CancellationToken cancellationToken) + { + return await attendeeById.LoadRequiredAsync(AttendeeId, cancellationToken); + } + + public async Task GetSessionAsync( + ISessionByIdDataLoader sessionById, + CancellationToken cancellationToken) + { + return await sessionById.LoadRequiredAsync(SessionId, cancellationToken); + } +} diff --git a/code/session-6/GraphQL/Common/Payload.cs b/code/session-6/GraphQL/Common/Payload.cs deleted file mode 100644 index e9d2839..0000000 --- a/code/session-6/GraphQL/Common/Payload.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace ConferencePlanner.GraphQL.Common -{ - public abstract class Payload - { - protected Payload(IReadOnlyList? errors = null) - { - Errors = errors; - } - - public IReadOnlyList? Errors { get; } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/Common/UserError.cs b/code/session-6/GraphQL/Common/UserError.cs deleted file mode 100644 index 3d587dd..0000000 --- a/code/session-6/GraphQL/Common/UserError.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace ConferencePlanner.GraphQL.Common -{ - public class UserError - { - public UserError(string message, string code) - { - Message = message; - Code = code; - } - - public string Message { get; } - - public string Code { get; } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/Data/ApplicationDbContext.cs b/code/session-6/GraphQL/Data/ApplicationDbContext.cs index bbd7dda..5a2d633 100644 --- a/code/session-6/GraphQL/Data/ApplicationDbContext.cs +++ b/code/session-6/GraphQL/Data/ApplicationDbContext.cs @@ -1,38 +1,33 @@ - using Microsoft.EntityFrameworkCore; - - namespace ConferencePlanner.GraphQL.Data - { - public class ApplicationDbContext : DbContext - { - public ApplicationDbContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasIndex(a => a.UserName) - .IsUnique(); - - // Many-to-many: Session <-> Attendee - modelBuilder - .Entity() - .HasKey(ca => new { ca.SessionId, ca.AttendeeId }); - - // Many-to-many: Speaker <-> Session - modelBuilder - .Entity() - .HasKey(ss => new { ss.SessionId, ss.SpeakerId }); - } - - public DbSet Sessions { get; set; } = default!; - - public DbSet Tracks { get; set; } = default!; - - public DbSet Speakers { get; set; } = default!; - - public DbSet Attendees { get; set; } = default!; - } - } \ No newline at end of file +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Data; + +public sealed class ApplicationDbContext(DbContextOptions options) + : DbContext(options) +{ + public DbSet Attendees { get; init; } + + public DbSet Sessions { get; init; } + + public DbSet Speakers { get; init; } + + public DbSet Tracks { get; init; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .HasIndex(a => a.Username) + .IsUnique(); + + // Many-to-many: Session <-> Attendee + modelBuilder + .Entity() + .HasKey(sa => new { sa.SessionId, sa.AttendeeId }); + + // Many-to-many: Speaker <-> Session + modelBuilder + .Entity() + .HasKey(ss => new { ss.SessionId, ss.SpeakerId }); + } +} diff --git a/code/session-6/GraphQL/Data/Attendee.cs b/code/session-6/GraphQL/Data/Attendee.cs index e3f9ab0..304ec00 100644 --- a/code/session-6/GraphQL/Data/Attendee.cs +++ b/code/session-6/GraphQL/Data/Attendee.cs @@ -1,28 +1,23 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Attendee { - public class Attendee - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? FirstName { get; set; } + [StringLength(200)] + public required string FirstName { get; init; } - [Required] - [StringLength(200)] - public string? LastName { get; set; } + [StringLength(200)] + public required string LastName { get; init; } - [Required] - [StringLength(200)] - public string? UserName { get; set; } + [StringLength(200)] + public required string Username { get; init; } - [StringLength(256)] - public string? EmailAddress { get; set; } + [StringLength(256)] + public string? EmailAddress { get; init; } - public ICollection SessionsAttendees { get; set; } = - new List(); - } -} \ No newline at end of file + public ICollection SessionsAttendees { get; init; } = + new List(); +} diff --git a/code/session-6/GraphQL/Data/Session.cs b/code/session-6/GraphQL/Data/Session.cs index b340977..086d57a 100644 --- a/code/session-6/GraphQL/Data/Session.cs +++ b/code/session-6/GraphQL/Data/Session.cs @@ -1,37 +1,33 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Session { - public class Session - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? Title { get; set; } + [StringLength(200)] + public required string Title { get; init; } - [StringLength(4000)] - public string? Abstract { get; set; } + [StringLength(4000)] + public string? Abstract { get; init; } - public DateTimeOffset? StartTime { get; set; } + public DateTimeOffset? StartTime { get; set; } - public DateTimeOffset? EndTime { get; set; } + public DateTimeOffset? EndTime { get; set; } - // Bonus points to those who can figure out why this is written this way - public TimeSpan Duration => - EndTime?.Subtract(StartTime ?? EndTime ?? DateTimeOffset.MinValue) ?? - TimeSpan.Zero; + // Bonus points to those who can figure out why this is written this way. + public TimeSpan Duration => + EndTime?.Subtract(StartTime ?? EndTime ?? DateTimeOffset.MinValue) ?? + TimeSpan.Zero; - public int? TrackId { get; set; } + public int? TrackId { get; set; } - public ICollection SessionSpeakers { get; set; } = - new List(); + public ICollection SessionSpeakers { get; init; } = + new List(); - public ICollection SessionAttendees { get; set; } = - new List(); + public ICollection SessionAttendees { get; init; } = + new List(); - public Track? Track { get; set; } - } -} \ No newline at end of file + public Track? Track { get; init; } +} diff --git a/code/session-6/GraphQL/Data/SessionAttendee.cs b/code/session-6/GraphQL/Data/SessionAttendee.cs index 089c71a..892d5ae 100644 --- a/code/session-6/GraphQL/Data/SessionAttendee.cs +++ b/code/session-6/GraphQL/Data/SessionAttendee.cs @@ -1,13 +1,12 @@ -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class SessionAttendee { - public class SessionAttendee - { - public int SessionId { get; set; } + public int SessionId { get; init; } - public Session? Session { get; set; } + public Session Session { get; init; } = null!; - public int AttendeeId { get; set; } + public int AttendeeId { get; init; } - public Attendee? Attendee { get; set; } - } -} \ No newline at end of file + public Attendee Attendee { get; init; } = null!; +} diff --git a/code/session-6/GraphQL/Data/SessionSpeaker.cs b/code/session-6/GraphQL/Data/SessionSpeaker.cs index ed83e86..aeebe62 100644 --- a/code/session-6/GraphQL/Data/SessionSpeaker.cs +++ b/code/session-6/GraphQL/Data/SessionSpeaker.cs @@ -1,13 +1,12 @@ -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class SessionSpeaker { - public class SessionSpeaker - { - public int SessionId { get; set; } + public int SessionId { get; init; } - public Session? Session { get; set; } + public Session Session { get; init; } = null!; - public int SpeakerId { get; set; } + public int SpeakerId { get; init; } - public Speaker? Speaker { get; set; } - } -} \ No newline at end of file + public Speaker Speaker { get; init; } = null!; +} diff --git a/code/session-6/GraphQL/Data/Speaker.cs b/code/session-6/GraphQL/Data/Speaker.cs index 0943514..bf47876 100644 --- a/code/session-6/GraphQL/Data/Speaker.cs +++ b/code/session-6/GraphQL/Data/Speaker.cs @@ -1,23 +1,20 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Speaker { - public class Speaker - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? Name { get; set; } + [StringLength(200)] + public required string Name { get; init; } - [StringLength(4000)] - public string? Bio { get; set; } + [StringLength(4000)] + public string? Bio { get; init; } - [StringLength(1000)] - public string? WebSite { get; set; } + [StringLength(1000)] + public string? Website { get; init; } - public ICollection SessionSpeakers { get; set; } = - new List(); - } - } \ No newline at end of file + public ICollection SessionSpeakers { get; init; } = + new List(); +} diff --git a/code/session-6/GraphQL/Data/Track.cs b/code/session-6/GraphQL/Data/Track.cs index f2392b6..51bd27b 100644 --- a/code/session-6/GraphQL/Data/Track.cs +++ b/code/session-6/GraphQL/Data/Track.cs @@ -1,17 +1,14 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Track { - public class Track - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? Name { get; set; } + [StringLength(200)] + public required string Name { get; set; } - public ICollection Sessions { get; set; } = - new List(); - } -} \ No newline at end of file + public ICollection Sessions { get; init; } = + new List(); +} diff --git a/code/session-6/GraphQL/DataLoader/AttendeeByIdDataLoader.cs b/code/session-6/GraphQL/DataLoader/AttendeeByIdDataLoader.cs deleted file mode 100644 index a2bdbd0..0000000 --- a/code/session-6/GraphQL/DataLoader/AttendeeByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class AttendeeByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public AttendeeByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Attendees - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/DataLoader/SessionByIdDataLoader.cs b/code/session-6/GraphQL/DataLoader/SessionByIdDataLoader.cs deleted file mode 100644 index dbd675b..0000000 --- a/code/session-6/GraphQL/DataLoader/SessionByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class SessionByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public SessionByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Sessions - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/DataLoader/SpeakerByIdDataLoader.cs b/code/session-6/GraphQL/DataLoader/SpeakerByIdDataLoader.cs deleted file mode 100644 index 44d8208..0000000 --- a/code/session-6/GraphQL/DataLoader/SpeakerByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class SpeakerByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public SpeakerByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Speakers - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/DataLoader/TrackByIdDataLoader.cs b/code/session-6/GraphQL/DataLoader/TrackByIdDataLoader.cs deleted file mode 100644 index 4db1f95..0000000 --- a/code/session-6/GraphQL/DataLoader/TrackByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class TrackByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public TrackByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Tracks - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs b/code/session-6/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs index 370c767..49c13a8 100644 --- a/code/session-6/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs +++ b/code/session-6/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs @@ -1,32 +1,17 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using HotChocolate.Types; +namespace ConferencePlanner.GraphQL.Extensions; -namespace ConferencePlanner.GraphQL +public static class ObjectFieldDescriptorExtensions { - public static class ObjectFieldDescriptorExtensions + public static IObjectFieldDescriptor UseUpperCase(this IObjectFieldDescriptor descriptor) { - public static IObjectFieldDescriptor UseDbContext( - this IObjectFieldDescriptor descriptor) - where TDbContext : DbContext + return descriptor.Use(next => async context => { - return descriptor.UseScopedService( - create: s => s.GetRequiredService>().CreateDbContext(), - disposeAsync: (s, c) => c.DisposeAsync()); - } + await next(context); - public static IObjectFieldDescriptor UseUpperCase( - this IObjectFieldDescriptor descriptor) - { - return descriptor.Use(next => async context => + if (context.Result is string s) { - await next(context); - - if (context.Result is string s) - { - context.Result = s.ToUpperInvariant(); - } - }); - } + context.Result = s.ToUpperInvariant(); + } + }); } -} \ No newline at end of file +} diff --git a/code/session-6/GraphQL/Extensions/UseApplicationDbContextAttribute.cs b/code/session-6/GraphQL/Extensions/UseApplicationDbContextAttribute.cs deleted file mode 100644 index 79c9907..0000000 --- a/code/session-6/GraphQL/Extensions/UseApplicationDbContextAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Reflection; -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types; -using HotChocolate.Types.Descriptors; - -namespace ConferencePlanner.GraphQL -{ - public class UseApplicationDbContextAttribute : ObjectFieldDescriptorAttribute - { - public override void OnConfigure( - IDescriptorContext context, - IObjectFieldDescriptor descriptor, - MemberInfo member) - { - descriptor.UseDbContext(); - } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/Extensions/UseUpperCaseAttribute.cs b/code/session-6/GraphQL/Extensions/UseUpperCaseAttribute.cs index 8376b5b..b85152d 100644 --- a/code/session-6/GraphQL/Extensions/UseUpperCaseAttribute.cs +++ b/code/session-6/GraphQL/Extensions/UseUpperCaseAttribute.cs @@ -1,17 +1,15 @@ -using HotChocolate.Types; -using HotChocolate.Types.Descriptors; using System.Reflection; +using HotChocolate.Types.Descriptors; + +namespace ConferencePlanner.GraphQL.Extensions; -namespace ConferencePlanner.GraphQL +public sealed class UseUpperCaseAttribute : ObjectFieldDescriptorAttribute { - public class UseUpperCaseAttribute : ObjectFieldDescriptorAttribute + protected override void OnConfigure( + IDescriptorContext context, + IObjectFieldDescriptor descriptor, + MemberInfo member) { - public override void OnConfigure( - IDescriptorContext context, - IObjectFieldDescriptor descriptor, - MemberInfo member) - { - descriptor.UseUpperCase(); - } + descriptor.UseUpperCase(); } -} \ No newline at end of file +} diff --git a/code/session-6/GraphQL/GraphQL.csproj b/code/session-6/GraphQL/GraphQL.csproj index 0a3a4a0..f079203 100644 --- a/code/session-6/GraphQL/GraphQL.csproj +++ b/code/session-6/GraphQL/GraphQL.csproj @@ -1,19 +1,25 @@ - - - - net5.0 - ConferencePlanner.GraphQL - enable - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - + + + + net8.0 + enable + enable + ConferencePlanner.GraphQL + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/code/session-6/GraphQL/Migrations/20201010183502_Initial.Designer.cs b/code/session-6/GraphQL/Migrations/20201010183502_Initial.Designer.cs deleted file mode 100644 index dc170c1..0000000 --- a/code/session-6/GraphQL/Migrations/20201010183502_Initial.Designer.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -using ConferencePlanner.GraphQL.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace GraphQL.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20201010183502_Initial")] - partial class Initial - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bio") - .HasMaxLength(4000) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("WebSite") - .HasMaxLength(1000) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Speakers"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/code/session-6/GraphQL/Migrations/20201010183502_Initial.cs b/code/session-6/GraphQL/Migrations/20201010183502_Initial.cs deleted file mode 100644 index 69e30fb..0000000 --- a/code/session-6/GraphQL/Migrations/20201010183502_Initial.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace GraphQL.Migrations -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Speakers", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), - Bio = table.Column(type: "TEXT", maxLength: 4000, nullable: true), - WebSite = table.Column(type: "TEXT", maxLength: 1000, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Speakers", x => x.Id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Speakers"); - } - } -} diff --git a/code/session-6/GraphQL/Migrations/20240807140835_Initial.Designer.cs b/code/session-6/GraphQL/Migrations/20240807140835_Initial.Designer.cs new file mode 100644 index 0000000..d508064 --- /dev/null +++ b/code/session-6/GraphQL/Migrations/20240807140835_Initial.Designer.cs @@ -0,0 +1,55 @@ +// +using ConferencePlanner.GraphQL.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240807140835_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Bio") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Website") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.HasKey("Id"); + + b.ToTable("Speakers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/code/session-6/GraphQL/Migrations/20240807140835_Initial.cs b/code/session-6/GraphQL/Migrations/20240807140835_Initial.cs new file mode 100644 index 0000000..7301c7a --- /dev/null +++ b/code/session-6/GraphQL/Migrations/20240807140835_Initial.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Speakers", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Bio = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + Website = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Speakers", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Speakers"); + } + } +} diff --git a/code/session-6/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs b/code/session-6/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs new file mode 100644 index 0000000..75c788f --- /dev/null +++ b/code/session-6/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs @@ -0,0 +1,241 @@ +// +using System; +using ConferencePlanner.GraphQL.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240812080119_Refactoring")] + partial class Refactoring + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EmailAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Attendees"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Abstract") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TrackId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.ToTable("Sessions"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => + { + b.Property("SessionId") + .HasColumnType("integer"); + + b.Property("AttendeeId") + .HasColumnType("integer"); + + b.HasKey("SessionId", "AttendeeId"); + + b.HasIndex("AttendeeId"); + + b.ToTable("SessionAttendee"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => + { + b.Property("SessionId") + .HasColumnType("integer"); + + b.Property("SpeakerId") + .HasColumnType("integer"); + + b.HasKey("SessionId", "SpeakerId"); + + b.HasIndex("SpeakerId"); + + b.ToTable("SessionSpeaker"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Bio") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Website") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.HasKey("Id"); + + b.ToTable("Speakers"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => + { + b.HasOne("ConferencePlanner.GraphQL.Data.Track", "Track") + .WithMany("Sessions") + .HasForeignKey("TrackId"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => + { + b.HasOne("ConferencePlanner.GraphQL.Data.Attendee", "Attendee") + .WithMany("SessionsAttendees") + .HasForeignKey("AttendeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ConferencePlanner.GraphQL.Data.Session", "Session") + .WithMany("SessionAttendees") + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Attendee"); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => + { + b.HasOne("ConferencePlanner.GraphQL.Data.Session", "Session") + .WithMany("SessionSpeakers") + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ConferencePlanner.GraphQL.Data.Speaker", "Speaker") + .WithMany("SessionSpeakers") + .HasForeignKey("SpeakerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Session"); + + b.Navigation("Speaker"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => + { + b.Navigation("SessionsAttendees"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => + { + b.Navigation("SessionAttendees"); + + b.Navigation("SessionSpeakers"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => + { + b.Navigation("SessionSpeakers"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Track", b => + { + b.Navigation("Sessions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/code/session-6/GraphQL/Migrations/20240812080119_Refactoring.cs b/code/session-6/GraphQL/Migrations/20240812080119_Refactoring.cs new file mode 100644 index 0000000..e544f2e --- /dev/null +++ b/code/session-6/GraphQL/Migrations/20240812080119_Refactoring.cs @@ -0,0 +1,155 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations +{ + /// + public partial class Refactoring : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Attendees", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + FirstName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + LastName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Username = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + EmailAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Attendees", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Tracks", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tracks", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Sessions", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Abstract = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + StartTime = table.Column(type: "timestamp with time zone", nullable: true), + EndTime = table.Column(type: "timestamp with time zone", nullable: true), + TrackId = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Sessions", x => x.Id); + table.ForeignKey( + name: "FK_Sessions_Tracks_TrackId", + column: x => x.TrackId, + principalTable: "Tracks", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "SessionAttendee", + columns: table => new + { + SessionId = table.Column(type: "integer", nullable: false), + AttendeeId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SessionAttendee", x => new { x.SessionId, x.AttendeeId }); + table.ForeignKey( + name: "FK_SessionAttendee_Attendees_AttendeeId", + column: x => x.AttendeeId, + principalTable: "Attendees", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_SessionAttendee_Sessions_SessionId", + column: x => x.SessionId, + principalTable: "Sessions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "SessionSpeaker", + columns: table => new + { + SessionId = table.Column(type: "integer", nullable: false), + SpeakerId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SessionSpeaker", x => new { x.SessionId, x.SpeakerId }); + table.ForeignKey( + name: "FK_SessionSpeaker_Sessions_SessionId", + column: x => x.SessionId, + principalTable: "Sessions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_SessionSpeaker_Speakers_SpeakerId", + column: x => x.SpeakerId, + principalTable: "Speakers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Attendees_Username", + table: "Attendees", + column: "Username", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_SessionAttendee_AttendeeId", + table: "SessionAttendee", + column: "AttendeeId"); + + migrationBuilder.CreateIndex( + name: "IX_Sessions_TrackId", + table: "Sessions", + column: "TrackId"); + + migrationBuilder.CreateIndex( + name: "IX_SessionSpeaker_SpeakerId", + table: "SessionSpeaker", + column: "SpeakerId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SessionAttendee"); + + migrationBuilder.DropTable( + name: "SessionSpeaker"); + + migrationBuilder.DropTable( + name: "Attendees"); + + migrationBuilder.DropTable( + name: "Sessions"); + + migrationBuilder.DropTable( + name: "Tracks"); + } + } +} diff --git a/code/session-6/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs b/code/session-6/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs index a66dfe1..6cc21a5 100644 --- a/code/session-6/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/code/session-6/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs @@ -4,8 +4,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { [DbContext(typeof(ApplicationDbContext))] partial class ApplicationDbContextModelSnapshot : ModelSnapshot @@ -14,36 +17,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("EmailAddress") .HasMaxLength(256) - .HasColumnType("TEXT"); + .HasColumnType("character varying(256)"); b.Property("FirstName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("LastName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("UserName") + b.Property("Username") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); - b.HasIndex("UserName") + b.HasIndex("Username") .IsUnique(); b.ToTable("Attendees"); @@ -53,25 +61,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Abstract") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("EndTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("StartTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("Title") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("TrackId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("Id"); @@ -83,10 +93,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("AttendeeId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "AttendeeId"); @@ -98,10 +108,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("SpeakerId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "SpeakerId"); @@ -114,20 +124,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Bio") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("WebSite") + b.Property("Website") .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(1000)"); b.HasKey("Id"); @@ -138,12 +150,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); diff --git a/code/session-6/GraphQL/Program.cs b/code/session-6/GraphQL/Program.cs index c4914c6..6598afb 100644 --- a/code/session-6/GraphQL/Program.cs +++ b/code/session-6/GraphQL/Program.cs @@ -1,26 +1,25 @@ -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 ConferencePlanner.GraphQL -{ - 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(); - }); - } -} +using ConferencePlanner.GraphQL.Data; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddDbContext( + options => options.UseNpgsql("Host=127.0.0.1;Username=graphql_workshop;Password=secret")) + .AddGraphQLServer() + .AddGlobalObjectIdentification() + .AddMutationConventions() + .AddDbContextCursorPagingProvider() + .AddPagingArguments() + .AddFiltering() + .AddSorting() + .AddRedisSubscriptions(_ => ConnectionMultiplexer.Connect("127.0.0.1:6379")) + .AddGraphQLTypes(); + +var app = builder.Build(); + +app.UseWebSockets(); +app.MapGraphQL(); + +await app.RunWithGraphQLCommandsAsync(args); diff --git a/code/session-6/GraphQL/Properties/launchSettings.json b/code/session-6/GraphQL/Properties/launchSettings.json new file mode 100644 index 0000000..c0d9484 --- /dev/null +++ b/code/session-6/GraphQL/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7000;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/code/session-6/GraphQL/Sessions/AddSessionInput.cs b/code/session-6/GraphQL/Sessions/AddSessionInput.cs index db5995f..3474bf3 100644 --- a/code/session-6/GraphQL/Sessions/AddSessionInput.cs +++ b/code/session-6/GraphQL/Sessions/AddSessionInput.cs @@ -1,12 +1,8 @@ -using System.Collections.Generic; using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; -namespace ConferencePlanner.GraphQL.Sessions -{ - public record AddSessionInput( - string Title, - string? Abstract, - [ID(nameof(Speaker))] - IReadOnlyList SpeakerIds); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed record AddSessionInput( + string Title, + string? Abstract, + [property: ID] IReadOnlyList SpeakerIds); diff --git a/code/session-6/GraphQL/Sessions/AddSessionPayload.cs b/code/session-6/GraphQL/Sessions/AddSessionPayload.cs deleted file mode 100644 index 82775f8..0000000 --- a/code/session-6/GraphQL/Sessions/AddSessionPayload.cs +++ /dev/null @@ -1,20 +0,0 @@ -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class AddSessionPayload : Payload - { - public AddSessionPayload(Session session) - { - Session = session; - } - - public AddSessionPayload(UserError error) - : base(new[] { error }) - { - } - - public Session? Session { get; init; } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/Sessions/ScheduleSessionInput.cs b/code/session-6/GraphQL/Sessions/ScheduleSessionInput.cs index fc43463..9c4fd11 100644 --- a/code/session-6/GraphQL/Sessions/ScheduleSessionInput.cs +++ b/code/session-6/GraphQL/Sessions/ScheduleSessionInput.cs @@ -1,14 +1,9 @@ -using System; using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; -namespace ConferencePlanner.GraphQL.Sessions -{ - public record ScheduleSessionInput( - [ID(nameof(Session))] - int SessionId, - [ID(nameof(Track))] - int TrackId, - DateTimeOffset StartTime, - DateTimeOffset EndTime); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed record ScheduleSessionInput( + [property: ID] int SessionId, + [property: ID] int TrackId, + DateTimeOffset StartTime, + DateTimeOffset EndTime); diff --git a/code/session-6/GraphQL/Sessions/ScheduleSessionPayload.cs b/code/session-6/GraphQL/Sessions/ScheduleSessionPayload.cs deleted file mode 100644 index ce79df5..0000000 --- a/code/session-6/GraphQL/Sessions/ScheduleSessionPayload.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class ScheduleSessionPayload : SessionPayloadBase - { - public ScheduleSessionPayload(Session session) - : base(session) - { - } - - public ScheduleSessionPayload(UserError error) - : base(new[] { error }) - { - } - - public async Task GetTrackAsync( - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) - { - if (Session is null) - { - return null; - } - - return await trackById.LoadAsync(Session.Id, cancellationToken); - } - - [UseApplicationDbContext] - public async Task?> GetSpeakersAsync( - [ScopedService] ApplicationDbContext dbContext, - SpeakerByIdDataLoader speakerById, - CancellationToken cancellationToken) - { - if (Session is null) - { - return null; - } - - int[] speakerIds = await dbContext.Sessions - .Where(s => s.Id == Session.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SpeakerId)) - .ToArrayAsync(); - - return await speakerById.LoadAsync(speakerIds, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/Sessions/SessionDataLoaders.cs b/code/session-6/GraphQL/Sessions/SessionDataLoaders.cs new file mode 100644 index 0000000..738ce5e --- /dev/null +++ b/code/session-6/GraphQL/Sessions/SessionDataLoaders.cs @@ -0,0 +1,50 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Sessions; + +public static class SessionDataLoaders +{ + [DataLoader] + public static async Task> SessionByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => ids.Contains(s.Id)) + .Select(s => s.Id, selector) + .ToDictionaryAsync(s => s.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SpeakersBySessionIdAsync( + IReadOnlyList sessionIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => sessionIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionSpeakers.Select(ss => ss.Speaker), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } + + [DataLoader] + public static async Task> AttendeesBySessionIdAsync( + IReadOnlyList sessionIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => sessionIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionAttendees.Select(sa => sa.Attendee), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-6/GraphQL/Sessions/SessionExceptions.cs b/code/session-6/GraphQL/Sessions/SessionExceptions.cs new file mode 100644 index 0000000..fea5d77 --- /dev/null +++ b/code/session-6/GraphQL/Sessions/SessionExceptions.cs @@ -0,0 +1,9 @@ +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed class EndTimeInvalidException() : Exception("EndTime must be after StartTime."); + +public sealed class NoSpeakerException() : Exception("No speaker assigned."); + +public sealed class SessionNotFoundException() : Exception("Session not found."); + +public sealed class TitleEmptyException() : Exception("The title cannot be empty."); diff --git a/code/session-6/GraphQL/Sessions/SessionFilterInputType.cs b/code/session-6/GraphQL/Sessions/SessionFilterInputType.cs new file mode 100644 index 0000000..b6096b6 --- /dev/null +++ b/code/session-6/GraphQL/Sessions/SessionFilterInputType.cs @@ -0,0 +1,17 @@ +using ConferencePlanner.GraphQL.Data; +using HotChocolate.Data.Filters; + +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed class SessionFilterInputType : FilterInputType +{ + protected override void Configure(IFilterInputTypeDescriptor descriptor) + { + descriptor.BindFieldsExplicitly(); + + descriptor.Field(s => s.Title); + descriptor.Field(s => s.Abstract); + descriptor.Field(s => s.StartTime); + descriptor.Field(s => s.EndTime); + } +} diff --git a/code/session-6/GraphQL/Sessions/SessionMutations.cs b/code/session-6/GraphQL/Sessions/SessionMutations.cs index 74ca479..5986337 100644 --- a/code/session-6/GraphQL/Sessions/SessionMutations.cs +++ b/code/session-6/GraphQL/Sessions/SessionMutations.cs @@ -1,80 +1,80 @@ -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Common; using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; +using HotChocolate.Subscriptions; -namespace ConferencePlanner.GraphQL.Sessions +namespace ConferencePlanner.GraphQL.Sessions; + +[MutationType] +public static class SessionMutations { - [ExtendObjectType(Name = "Mutation")] - public class SessionMutations + [Error] + [Error] + public static async Task AddSessionAsync( + AddSessionInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - [UseApplicationDbContext] - public async Task AddSessionAsync( - AddSessionInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) + if (string.IsNullOrEmpty(input.Title)) { - if (string.IsNullOrEmpty(input.Title)) - { - return new AddSessionPayload( - new UserError("The title cannot be empty.", "TITLE_EMPTY")); - } + throw new TitleEmptyException(); + } - if (input.SpeakerIds.Count == 0) - { - return new AddSessionPayload( - new UserError("No speaker assigned.", "NO_SPEAKER")); - } + if (input.SpeakerIds.Count == 0) + { + throw new NoSpeakerException(); + } - var session = new Session - { - Title = input.Title, - Abstract = input.Abstract, - }; + var session = new Session + { + Title = input.Title, + Abstract = input.Abstract + }; - foreach (int speakerId in input.SpeakerIds) + foreach (var speakerId in input.SpeakerIds) + { + session.SessionSpeakers.Add(new SessionSpeaker { - session.SessionSpeakers.Add(new SessionSpeaker - { - SpeakerId = speakerId - }); - } + SpeakerId = speakerId + }); + } - context.Sessions.Add(session); - await context.SaveChangesAsync(cancellationToken); + dbContext.Sessions.Add(session); - return new AddSessionPayload(session); - } + await dbContext.SaveChangesAsync(cancellationToken); - [UseApplicationDbContext] - public async Task ScheduleSessionAsync( - ScheduleSessionInput input, - [ScopedService] ApplicationDbContext context) + return session; + } + + [Error] + [Error] + public static async Task ScheduleSessionAsync( + ScheduleSessionInput input, + ApplicationDbContext dbContext, + ITopicEventSender eventSender, + CancellationToken cancellationToken) + { + if (input.EndTime < input.StartTime) { - if (input.EndTime < input.StartTime) - { - return new ScheduleSessionPayload( - new UserError("endTime has to be larger than startTime.", "END_TIME_INVALID")); - } + throw new EndTimeInvalidException(); + } - Session session = await context.Sessions.FindAsync(input.SessionId); - int? initialTrackId = session.TrackId; + var session = await dbContext.Sessions.FindAsync([input.SessionId], cancellationToken); - if (session is null) - { - return new ScheduleSessionPayload( - new UserError("Session not found.", "SESSION_NOT_FOUND")); - } + if (session is null) + { + throw new SessionNotFoundException(); + } - session.TrackId = input.TrackId; - session.StartTime = input.StartTime; - session.EndTime = input.EndTime; + session.TrackId = input.TrackId; + session.StartTime = input.StartTime; + session.EndTime = input.EndTime; - await context.SaveChangesAsync(); + await dbContext.SaveChangesAsync(cancellationToken); - return new ScheduleSessionPayload(session); - } + await eventSender.SendAsync( + nameof(SessionSubscriptions.OnSessionScheduledAsync), + session.Id, + cancellationToken); + + return session; } -} \ No newline at end of file +} diff --git a/code/session-6/GraphQL/Sessions/SessionPayloadBase.cs b/code/session-6/GraphQL/Sessions/SessionPayloadBase.cs deleted file mode 100644 index 888ad50..0000000 --- a/code/session-6/GraphQL/Sessions/SessionPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class SessionPayloadBase : Payload - { - protected SessionPayloadBase(Session session) - { - Session = session; - } - - protected SessionPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Session? Session { get; } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/Sessions/SessionQueries.cs b/code/session-6/GraphQL/Sessions/SessionQueries.cs index 1cacdfd..7c1294c 100644 --- a/code/session-6/GraphQL/Sessions/SessionQueries.cs +++ b/code/session-6/GraphQL/Sessions/SessionQueries.cs @@ -1,40 +1,37 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; -using System.Linq; -using ConferencePlanner.GraphQL.Types; -using HotChocolate.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; -namespace ConferencePlanner.GraphQL.Sessions +namespace ConferencePlanner.GraphQL.Sessions; + +[QueryType] +public static class SessionQueries { - [ExtendObjectType(Name = "Query")] - public class SessionQueries + [UsePaging] + [UseFiltering] + [UseSorting] + public static IQueryable GetSessions(ApplicationDbContext dbContext) { - [UseApplicationDbContext] - [UsePaging(typeof(NonNullType))] - // TODO: [UseFiltering(typeof(SessionFilterInputType))] - [UseFiltering] - [UseSorting] - public IQueryable GetSessions( - [ScopedService] ApplicationDbContext context) => - context.Sessions; + return dbContext.Sessions.AsNoTracking().OrderBy(s => s.Title).ThenBy(s => s.Id); + } - public Task GetSessionByIdAsync( - [ID(nameof(Session))] int id, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - sessionById.LoadAsync(id, cancellationToken); + [NodeResolver] + public static async Task GetSessionByIdAsync( + int id, + ISessionByIdDataLoader sessionById, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionById.Select(selection).LoadAsync(id, cancellationToken); + } - public async Task> GetSessionsByIdAsync( - [ID(nameof(Session))] int[] ids, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - await sessionById.LoadAsync(ids, cancellationToken); + public static async Task> GetSessionsByIdAsync( + [ID] int[] ids, + ISessionByIdDataLoader sessionById, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionById.Select(selection).LoadRequiredAsync(ids, cancellationToken); } -} \ No newline at end of file +} diff --git a/code/session-6/GraphQL/Sessions/SessionSubscriptions.cs b/code/session-6/GraphQL/Sessions/SessionSubscriptions.cs new file mode 100644 index 0000000..5e54e24 --- /dev/null +++ b/code/session-6/GraphQL/Sessions/SessionSubscriptions.cs @@ -0,0 +1,17 @@ +using ConferencePlanner.GraphQL.Data; + +namespace ConferencePlanner.GraphQL.Sessions; + +[SubscriptionType] +public static class SessionSubscriptions +{ + [Subscribe] + [Topic] + public static async Task OnSessionScheduledAsync( + [EventMessage] int sessionId, + ISessionByIdDataLoader sessionById, + CancellationToken cancellationToken) + { + return await sessionById.LoadRequiredAsync(sessionId, cancellationToken); + } +} diff --git a/code/session-6/GraphQL/Sessions/SessionType.cs b/code/session-6/GraphQL/Sessions/SessionType.cs new file mode 100644 index 0000000..abda805 --- /dev/null +++ b/code/session-6/GraphQL/Sessions/SessionType.cs @@ -0,0 +1,60 @@ +using ConferencePlanner.GraphQL.Data; +using ConferencePlanner.GraphQL.Tracks; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Sessions; + +[ObjectType] +public static partial class SessionType +{ + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field(s => s.TrackId) + .ID(); + } + + public static TimeSpan Duration([Parent("StartTime EndTime")] Session session) + => session.Duration; + + [BindMember(nameof(Session.SessionSpeakers))] + public static async Task> GetSpeakersAsync( + [Parent] Session session, + ISpeakersBySessionIdDataLoader speakersBySessionId, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakersBySessionId + .Select(selection) + .LoadRequiredAsync(session.Id, cancellationToken); + } + + [BindMember(nameof(Session.SessionAttendees))] + public static async Task> GetAttendeesAsync( + [Parent(nameof(Session.Id))] Session session, + IAttendeesBySessionIdDataLoader attendeesBySessionId, + ISelection selection, + CancellationToken cancellationToken) + { + return await attendeesBySessionId + .Select(selection) + .LoadRequiredAsync(session.Id, cancellationToken); + } + + public static async Task GetTrackAsync( + [Parent(nameof(Session.TrackId))] Session session, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + if (session.TrackId is null) + { + return null; + } + + return await trackById + .Select(selection) + .LoadAsync(session.TrackId.Value, cancellationToken); + } +} diff --git a/code/session-6/GraphQL/Speakers/AddSpeakerInput.cs b/code/session-6/GraphQL/Speakers/AddSpeakerInput.cs index a81f45f..bdc584a 100644 --- a/code/session-6/GraphQL/Speakers/AddSpeakerInput.cs +++ b/code/session-6/GraphQL/Speakers/AddSpeakerInput.cs @@ -1,7 +1,6 @@ -namespace ConferencePlanner.GraphQL.Speakers -{ - public record AddSpeakerInput( - string Name, - string? Bio, - string? WebSite); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Speakers; + +public sealed record AddSpeakerInput( + string Name, + string? Bio, + string? Website); diff --git a/code/session-6/GraphQL/Speakers/AddSpeakerPayload.cs b/code/session-6/GraphQL/Speakers/AddSpeakerPayload.cs deleted file mode 100644 index aaf0ab0..0000000 --- a/code/session-6/GraphQL/Speakers/AddSpeakerPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Speakers -{ - public class AddSpeakerPayload : SpeakerPayloadBase - { - public AddSpeakerPayload(Speaker speaker) - : base(speaker) - { - } - - public AddSpeakerPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/Speakers/SpeakerDataLoaders.cs b/code/session-6/GraphQL/Speakers/SpeakerDataLoaders.cs new file mode 100644 index 0000000..c2c748e --- /dev/null +++ b/code/session-6/GraphQL/Speakers/SpeakerDataLoaders.cs @@ -0,0 +1,36 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Speakers; + +public static class SpeakerDataLoaders +{ + [DataLoader] + public static async Task> SpeakerByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Speakers + .AsNoTracking() + .Where(s => ids.Contains(s.Id)) + .Select(s => s.Id, selector) + .ToDictionaryAsync(s => s.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SessionsBySpeakerIdAsync( + IReadOnlyList speakerIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Speakers + .AsNoTracking() + .Where(s => speakerIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionSpeakers.Select(ss => ss.Session), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-6/GraphQL/Speakers/SpeakerMutations.cs b/code/session-6/GraphQL/Speakers/SpeakerMutations.cs index 55fc6f5..0a8ad7a 100644 --- a/code/session-6/GraphQL/Speakers/SpeakerMutations.cs +++ b/code/session-6/GraphQL/Speakers/SpeakerMutations.cs @@ -1,29 +1,26 @@ -using System.Threading.Tasks; using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; -namespace ConferencePlanner.GraphQL.Speakers +namespace ConferencePlanner.GraphQL.Speakers; + +[MutationType] +public static class SpeakerMutations { - [ExtendObjectType(Name = "Mutation")] - public class SpeakerMutations + public static async Task AddSpeakerAsync( + AddSpeakerInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - [UseApplicationDbContext] - public async Task AddSpeakerAsync( - AddSpeakerInput input, - [ScopedService] ApplicationDbContext context) + var speaker = new Speaker { - var speaker = new Speaker - { - Name = input.Name, - Bio = input.Bio, - WebSite = input.WebSite - }; + Name = input.Name, + Bio = input.Bio, + Website = input.Website + }; + + dbContext.Speakers.Add(speaker); - context.Speakers.Add(speaker); - await context.SaveChangesAsync(); + await dbContext.SaveChangesAsync(cancellationToken); - return new AddSpeakerPayload(speaker); - } + return speaker; } -} \ No newline at end of file +} diff --git a/code/session-6/GraphQL/Speakers/SpeakerPayloadBase.cs b/code/session-6/GraphQL/Speakers/SpeakerPayloadBase.cs deleted file mode 100644 index a2077d7..0000000 --- a/code/session-6/GraphQL/Speakers/SpeakerPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Speakers -{ - public class SpeakerPayloadBase : Payload - { - protected SpeakerPayloadBase(Speaker speaker) - { - Speaker = speaker; - } - - protected SpeakerPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Speaker? Speaker { get; init; } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/Speakers/SpeakerQueries.cs b/code/session-6/GraphQL/Speakers/SpeakerQueries.cs index c9b57b8..8283587 100644 --- a/code/session-6/GraphQL/Speakers/SpeakerQueries.cs +++ b/code/session-6/GraphQL/Speakers/SpeakerQueries.cs @@ -1,35 +1,35 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; -using System.Linq; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; -namespace ConferencePlanner.GraphQL.Speakers +namespace ConferencePlanner.GraphQL.Speakers; + +[QueryType] +public static class SpeakerQueries { - [ExtendObjectType(Name = "Query")] - public class SpeakerQueries + [UsePaging] + public static IQueryable GetSpeakers(ApplicationDbContext dbContext) { - [UseApplicationDbContext] - [UsePaging] - public IQueryable GetSpeakers( - [ScopedService] ApplicationDbContext context) => - context.Speakers.OrderBy(t => t.Name); + return dbContext.Speakers.AsNoTracking().OrderBy(s => s.Name).ThenBy(s => s.Id); + } - public Task GetSpeakerByIdAsync( - [ID(nameof(Speaker))]int id, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - dataLoader.LoadAsync(id, cancellationToken); + [NodeResolver] + public static async Task GetSpeakerByIdAsync( + int id, + ISpeakerByIdDataLoader speakerById, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakerById.Select(selection).LoadAsync(id, cancellationToken); + } - public async Task> GetSpeakersByIdAsync( - [ID(nameof(Speaker))]int[] ids, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - await dataLoader.LoadAsync(ids, cancellationToken); + public static async Task> GetSpeakersByIdAsync( + [ID] int[] ids, + ISpeakerByIdDataLoader speakerById, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakerById.Select(selection).LoadRequiredAsync(ids, cancellationToken); } -} \ No newline at end of file +} diff --git a/code/session-6/GraphQL/Speakers/SpeakerType.cs b/code/session-6/GraphQL/Speakers/SpeakerType.cs new file mode 100644 index 0000000..54555fd --- /dev/null +++ b/code/session-6/GraphQL/Speakers/SpeakerType.cs @@ -0,0 +1,21 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Speakers; + +[ObjectType] +public static partial class SpeakerType +{ + [BindMember(nameof(Speaker.SessionSpeakers))] + public static async Task> GetSessionsAsync( + [Parent] Speaker speaker, + ISessionsBySpeakerIdDataLoader sessionsBySpeakerId, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsBySpeakerId + .Select(selection) + .LoadRequiredAsync(speaker.Id, cancellationToken); + } +} diff --git a/code/session-6/GraphQL/Startup.cs b/code/session-6/GraphQL/Startup.cs deleted file mode 100644 index ec2b08a..0000000 --- a/code/session-6/GraphQL/Startup.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using ConferencePlanner.GraphQL.Sessions; -using ConferencePlanner.GraphQL.Speakers; -using ConferencePlanner.GraphQL.Tracks; -using ConferencePlanner.GraphQL.Types; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace ConferencePlanner.GraphQL -{ - 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.AddPooledDbContextFactory( - options => options.UseSqlite("Data Source=conferences.db")); - - services - .AddGraphQLServer() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport() - .AddFiltering() - .AddSorting() - .AddDataLoader() - .AddDataLoader(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGraphQL(); - }); - } - } -} diff --git a/code/session-6/GraphQL/Tracks/AddTrackInput.cs b/code/session-6/GraphQL/Tracks/AddTrackInput.cs index 5c83b34..1aaf313 100644 --- a/code/session-6/GraphQL/Tracks/AddTrackInput.cs +++ b/code/session-6/GraphQL/Tracks/AddTrackInput.cs @@ -1,4 +1,3 @@ -namespace ConferencePlanner.GraphQL.Tracks -{ - public record AddTrackInput(string Name); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Tracks; + +public sealed record AddTrackInput(string Name); diff --git a/code/session-6/GraphQL/Tracks/AddTrackPayload.cs b/code/session-6/GraphQL/Tracks/AddTrackPayload.cs deleted file mode 100644 index 8f35b13..0000000 --- a/code/session-6/GraphQL/Tracks/AddTrackPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class AddTrackPayload : TrackPayloadBase - { - public AddTrackPayload(Track track) - : base(track) - { - } - - public AddTrackPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/Tracks/RenameTrackInput.cs b/code/session-6/GraphQL/Tracks/RenameTrackInput.cs index 516c6a0..d11ad39 100644 --- a/code/session-6/GraphQL/Tracks/RenameTrackInput.cs +++ b/code/session-6/GraphQL/Tracks/RenameTrackInput.cs @@ -1,7 +1,5 @@ using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; -namespace ConferencePlanner.GraphQL.Tracks -{ - public record RenameTrackInput([ID(nameof(Track))] int Id, string Name); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Tracks; + +public sealed record RenameTrackInput([property: ID] int Id, string Name); diff --git a/code/session-6/GraphQL/Tracks/RenameTrackPayload.cs b/code/session-6/GraphQL/Tracks/RenameTrackPayload.cs deleted file mode 100644 index ca4c8a1..0000000 --- a/code/session-6/GraphQL/Tracks/RenameTrackPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class RenameTrackPayload : TrackPayloadBase - { - public RenameTrackPayload(Track track) - : base(track) - { - } - - public RenameTrackPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/Tracks/TrackDataLoaders.cs b/code/session-6/GraphQL/Tracks/TrackDataLoaders.cs new file mode 100644 index 0000000..f592b8a --- /dev/null +++ b/code/session-6/GraphQL/Tracks/TrackDataLoaders.cs @@ -0,0 +1,38 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Tracks; + +public static class TrackDataLoaders +{ + [DataLoader] + public static async Task> TrackByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Tracks + .AsNoTracking() + .Where(t => ids.Contains(t.Id)) + .Select(t => t.Id, selector) + .ToDictionaryAsync(t => t.Id, cancellationToken); + } + + [DataLoader] + public static async Task>> SessionsByTrackIdAsync( + IReadOnlyList trackIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + PagingArguments pagingArguments, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => s.TrackId != null && trackIds.Contains((int)s.TrackId)) + .OrderBy(s => s.Id) + .Select(s => s.TrackId, selector) + .ToBatchPageAsync(s => (int)s.TrackId!, pagingArguments, cancellationToken); + } +} diff --git a/code/session-6/GraphQL/Tracks/TrackExceptions.cs b/code/session-6/GraphQL/Tracks/TrackExceptions.cs new file mode 100644 index 0000000..8df488d --- /dev/null +++ b/code/session-6/GraphQL/Tracks/TrackExceptions.cs @@ -0,0 +1,3 @@ +namespace ConferencePlanner.GraphQL.Tracks; + +public sealed class TrackNotFoundException() : Exception("Track not found."); diff --git a/code/session-6/GraphQL/Tracks/TrackMutations.cs b/code/session-6/GraphQL/Tracks/TrackMutations.cs index 88727b9..a91671c 100644 --- a/code/session-6/GraphQL/Tracks/TrackMutations.cs +++ b/code/session-6/GraphQL/Tracks/TrackMutations.cs @@ -1,40 +1,41 @@ -using System.Threading; -using System.Threading.Tasks; using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; -namespace ConferencePlanner.GraphQL.Tracks +namespace ConferencePlanner.GraphQL.Tracks; + +[MutationType] +public static class TrackMutations { - [ExtendObjectType(Name = "Mutation")] - public class TrackMutations + public static async Task AddTrackAsync( + AddTrackInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - [UseApplicationDbContext] - public async Task AddTrackAsync( - AddTrackInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - var track = new Track { Name = input.Name }; - context.Tracks.Add(track); + var track = new Track { Name = input.Name }; - await context.SaveChangesAsync(cancellationToken); + dbContext.Tracks.Add(track); - return new AddTrackPayload(track); - } + await dbContext.SaveChangesAsync(cancellationToken); - [UseApplicationDbContext] - public async Task RenameTrackAsync( - RenameTrackInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - Track track = await context.Tracks.FindAsync(input.Id); - track.Name = input.Name; + return track; + } - await context.SaveChangesAsync(cancellationToken); + [Error] + public static async Task RenameTrackAsync( + RenameTrackInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var track = await dbContext.Tracks.FindAsync([input.Id], cancellationToken); - return new RenameTrackPayload(track); + if (track is null) + { + throw new TrackNotFoundException(); } + + track.Name = input.Name; + + await dbContext.SaveChangesAsync(cancellationToken); + + return track; } -} \ No newline at end of file +} diff --git a/code/session-6/GraphQL/Tracks/TrackPayloadBase.cs b/code/session-6/GraphQL/Tracks/TrackPayloadBase.cs deleted file mode 100644 index de11da7..0000000 --- a/code/session-6/GraphQL/Tracks/TrackPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class TrackPayloadBase : Payload - { - public TrackPayloadBase(Track track) - { - Track = track; - } - - public TrackPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Track? Track { get; } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/Tracks/TrackQueries.cs b/code/session-6/GraphQL/Tracks/TrackQueries.cs index 7ced249..948efac 100644 --- a/code/session-6/GraphQL/Tracks/TrackQueries.cs +++ b/code/session-6/GraphQL/Tracks/TrackQueries.cs @@ -1,49 +1,35 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Tracks; -namespace ConferencePlanner.GraphQL.Tracks +[QueryType] +public static class TrackQueries { - [ExtendObjectType(Name = "Query")] - public class TrackQueries + [UsePaging] + public static IQueryable GetTracks(ApplicationDbContext dbContext) { - [UseApplicationDbContext] - [UsePaging] - public IQueryable GetTracks( - [ScopedService] ApplicationDbContext context) => - context.Tracks.OrderBy(t => t.Name); - - [UseApplicationDbContext] - public Task GetTrackByNameAsync( - string name, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - context.Tracks.FirstAsync(t => t.Name == name); - - [UseApplicationDbContext] - public async Task> GetTrackByNamesAsync( - string[] names, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - await context.Tracks.Where(t => names.Contains(t.Name)).ToListAsync(); + return dbContext.Tracks.AsNoTracking().OrderBy(t => t.Name).ThenBy(t => t.Id); + } - public Task GetTrackByIdAsync( - [ID(nameof(Track))] int id, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) => - trackById.LoadAsync(id, cancellationToken); + [NodeResolver] + public static async Task GetTrackByIdAsync( + int id, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + return await trackById.Select(selection).LoadAsync(id, cancellationToken); + } - public async Task> GetTracksByIdAsync( - [ID(nameof(Track))] int[] ids, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) => - await trackById.LoadAsync(ids, cancellationToken); + public static async Task> GetTracksByIdAsync( + [ID] int[] ids, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + return await trackById.Select(selection).LoadRequiredAsync(ids, cancellationToken); } -} \ No newline at end of file +} diff --git a/code/session-6/GraphQL/Tracks/TrackType.cs b/code/session-6/GraphQL/Tracks/TrackType.cs new file mode 100644 index 0000000..2bc3559 --- /dev/null +++ b/code/session-6/GraphQL/Tracks/TrackType.cs @@ -0,0 +1,34 @@ +using ConferencePlanner.GraphQL.Data; +using ConferencePlanner.GraphQL.Extensions; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using HotChocolate.Types.Pagination; + +namespace ConferencePlanner.GraphQL.Tracks; + +[ObjectType] +public static partial class TrackType +{ + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field(t => t.Name) + .ParentRequires(nameof(Track.Name)) + .UseUpperCase(); + } + + [UsePaging] + public static async Task> GetSessionsAsync( + [Parent] Track track, + ISessionsByTrackIdDataLoader sessionsByTrackId, + PagingArguments pagingArguments, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsByTrackId + .With(pagingArguments) + .Select(selection) + .LoadAsync(track.Id, cancellationToken) + .ToConnectionAsync(); + } +} diff --git a/code/session-6/GraphQL/Types/AttendeeType.cs b/code/session-6/GraphQL/Types/AttendeeType.cs deleted file mode 100644 index 62274c7..0000000 --- a/code/session-6/GraphQL/Types/AttendeeType.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Types -{ - public class AttendeeType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionsAttendees) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("sessions"); - } - - private class AttendeeResolvers - { - public async Task> GetSessionsAsync( - Attendee attendee, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Attendees - .Where(a => a.Id == attendee.Id) - .Include(a => a.SessionsAttendees) - .SelectMany(a => a.SessionsAttendees.Select(t => t.SessionId)) - .ToArrayAsync(); - - return await sessionById.LoadAsync(speakerIds, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/Types/SessionFilterInputType.cs b/code/session-6/GraphQL/Types/SessionFilterInputType.cs deleted file mode 100644 index 9a514e8..0000000 --- a/code/session-6/GraphQL/Types/SessionFilterInputType.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Data.Filters; - -namespace ConferencePlanner.GraphQL.Types -{ - public class SessionFilterInputType : FilterInputType - { - protected override void Configure(IFilterInputTypeDescriptor descriptor) - { - descriptor.Ignore(t => t.Id); - descriptor.Ignore(t => t.TrackId); - } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/Types/SessionType.cs b/code/session-6/GraphQL/Types/SessionType.cs deleted file mode 100644 index 8a558f7..0000000 --- a/code/session-6/GraphQL/Types/SessionType.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Types -{ - public class SessionType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionSpeakers) - .ResolveWith(t => t.GetSpeakersAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("speakers"); - - descriptor - .Field(t => t.SessionAttendees) - .ResolveWith(t => t.GetAttendeesAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("attendees"); - - descriptor - .Field(t => t.Track) - .ResolveWith(t => t.GetTrackAsync(default!, default!, default)); - - descriptor - .Field(t => t.TrackId) - .ID(nameof(Track)); - } - - private class SessionResolvers - { - public async Task> GetSpeakersAsync( - Session session, - [ScopedService] ApplicationDbContext dbContext, - SpeakerByIdDataLoader speakerById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Sessions - .Where(s => s.Id == session.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SpeakerId)) - .ToArrayAsync(); - - return await speakerById.LoadAsync(speakerIds, cancellationToken); - } - - public async Task> GetAttendeesAsync( - Session session, - [ScopedService] ApplicationDbContext dbContext, - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) - { - int[] attendeeIds = await dbContext.Sessions - .Where(s => s.Id == session.Id) - .Include(session => session.SessionAttendees) - .SelectMany(session => session.SessionAttendees.Select(t => t.AttendeeId)) - .ToArrayAsync(); - - return await attendeeById.LoadAsync(attendeeIds, cancellationToken); - } - - public async Task GetTrackAsync( - Session session, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) - { - if (session.TrackId is null) - { - return null; - } - - return await trackById.LoadAsync(session.TrackId.Value, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/Types/SpeakerType.cs b/code/session-6/GraphQL/Types/SpeakerType.cs deleted file mode 100644 index 89837f0..0000000 --- a/code/session-6/GraphQL/Types/SpeakerType.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Types -{ - public class SpeakerType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionSpeakers) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("sessions"); - } - - private class SpeakerResolvers - { - public async Task> GetSessionsAsync( - Speaker speaker, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Speakers - .Where(s => s.Id == speaker.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SessionId)) - .ToArrayAsync(); - - return await sessionById.LoadAsync(speakerIds, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-6/GraphQL/Types/TrackType.cs b/code/session-6/GraphQL/Types/TrackType.cs deleted file mode 100644 index ecf3651..0000000 --- a/code/session-6/GraphQL/Types/TrackType.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Types -{ - public class TrackType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => - ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.Sessions) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .UsePaging>() - .Name("sessions"); - - descriptor - .Field(t => t.Name) - .UseUpperCase(); - } - - private class TrackResolvers - { - public async Task> GetSessionsAsync( - Track track, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] sessionIds = await dbContext.Sessions - .Where(s => s.TrackId == track.Id) - .Select(s => s.Id) - .ToArrayAsync(); - - return await sessionById.LoadAsync(sessionIds, cancellationToken); - } - } - } -} diff --git a/code/session-6/GraphQL/appsettings.Development.json b/code/session-6/GraphQL/appsettings.Development.json index dba68eb..0c208ae 100644 --- a/code/session-6/GraphQL/appsettings.Development.json +++ b/code/session-6/GraphQL/appsettings.Development.json @@ -1,9 +1,8 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Information", + "Microsoft.AspNetCore": "Warning" } } } diff --git a/code/session-6/GraphQL/appsettings.json b/code/session-6/GraphQL/appsettings.json index 81ff877..10f68b8 100644 --- a/code/session-6/GraphQL/appsettings.json +++ b/code/session-6/GraphQL/appsettings.json @@ -1,10 +1,9 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/code/session-6/docker-compose.yml b/code/session-6/docker-compose.yml new file mode 100644 index 0000000..5785c83 --- /dev/null +++ b/code/session-6/docker-compose.yml @@ -0,0 +1,33 @@ +name: graphql-workshop + +services: + graphql-workshop-postgres: + container_name: graphql-workshop-postgres + image: postgres:17.2 + environment: + POSTGRES_USER: graphql_workshop + POSTGRES_PASSWORD: secret + POSTGRES_DB: graphql_workshop + networks: [graphql-workshop] + ports: ["5432:5432"] + volumes: + - type: volume + source: postgres-data + target: /var/lib/postgresql/data + graphql-workshop-redis: + container_name: graphql-workshop-redis + image: redis:7.4 + networks: [graphql-workshop] + ports: ["6379:6379"] + volumes: + - type: volume + source: redis-data + target: /data + +networks: + graphql-workshop: + name: graphql-workshop + +volumes: + postgres-data: + redis-data: diff --git a/code/session-7/.config/dotnet-tools.json b/code/session-7/.config/dotnet-tools.json index c735fef..aad8137 100644 --- a/code/session-7/.config/dotnet-tools.json +++ b/code/session-7/.config/dotnet-tools.json @@ -3,10 +3,11 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "5.0.0", + "version": "9.0.1", "commands": [ "dotnet-ef" - ] + ], + "rollForward": false } } -} \ No newline at end of file +} diff --git a/code/session-7/.vscode/launch.json b/code/session-7/.vscode/launch.json deleted file mode 100644 index e90375e..0000000 --- a/code/session-7/.vscode/launch.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - // 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}/GraphQL/bin/Debug/net5.0/GraphQL.dll", - "args": [], - "cwd": "${workspaceFolder}/GraphQL", - "stopAtEntry": false, - "serverReadyAction": { - "action": "openExternally", - "pattern": "\\bNow 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/code/session-7/.vscode/tasks.json b/code/session-7/.vscode/tasks.json deleted file mode 100644 index 31c32bd..0000000 --- a/code/session-7/.vscode/tasks.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - // 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/code/session-7/ConferencePlanner.sln b/code/session-7/ConferencePlanner.sln index 42fadb8..47a083c 100644 --- a/code/session-7/ConferencePlanner.sln +++ b/code/session-7/ConferencePlanner.sln @@ -1,34 +1,28 @@ - -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}") = "GraphQL", "GraphQL\GraphQL.csproj", "{48385280-56F1-4937-9655-E6A79184740B}" -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 - {48385280-56F1-4937-9655-E6A79184740B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x64.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x64.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x86.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x86.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|Any CPU.Build.0 = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x64.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x64.Build.0 = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x86.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL", "GraphQL\GraphQL.csproj", "{D96823B9-86D3-4D54-A803-F1D43AEBE1FD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Tests", "GraphQL.Tests\GraphQL.Tests.csproj", "{5DE049BF-80BE-4408-9DA8-FD2576C1E088}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D96823B9-86D3-4D54-A803-F1D43AEBE1FD}.Release|Any CPU.Build.0 = Release|Any CPU + {5DE049BF-80BE-4408-9DA8-FD2576C1E088}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DE049BF-80BE-4408-9DA8-FD2576C1E088}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DE049BF-80BE-4408-9DA8-FD2576C1E088}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DE049BF-80BE-4408-9DA8-FD2576C1E088}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/code/session-7/GraphQL.Tests/AttendeeTests.cs b/code/session-7/GraphQL.Tests/AttendeeTests.cs new file mode 100644 index 0000000..e002faa --- /dev/null +++ b/code/session-7/GraphQL.Tests/AttendeeTests.cs @@ -0,0 +1,85 @@ +using ConferencePlanner.GraphQL.Data; +using CookieCrumble; +using HotChocolate.Execution; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using StackExchange.Redis; +using Testcontainers.PostgreSql; +using Testcontainers.Redis; + +namespace GraphQL.Tests; + +public sealed class AttendeeTests : IAsyncLifetime +{ + private readonly PostgreSqlContainer _postgreSqlContainer = new PostgreSqlBuilder() + .WithImage("postgres:17.2") + .Build(); + + private readonly RedisContainer _redisContainer = new RedisBuilder() + .WithImage("redis:7.4") + .Build(); + + private IRequestExecutor _requestExecutor = null!; + + public async ValueTask InitializeAsync() + { + // Start test containers. + await Task.WhenAll(_postgreSqlContainer.StartAsync(), _redisContainer.StartAsync()); + + // Build request executor. + _requestExecutor = await new ServiceCollection() + .AddDbContext( + options => options.UseNpgsql(_postgreSqlContainer.GetConnectionString())) + .AddGraphQLServer() + .AddGlobalObjectIdentification() + .AddMutationConventions() + .AddDbContextCursorPagingProvider() + .AddPagingArguments() + .AddFiltering() + .AddSorting() + .AddRedisSubscriptions( + _ => ConnectionMultiplexer.Connect(_redisContainer.GetConnectionString())) + .AddGraphQLTypes() + .BuildRequestExecutorAsync(); + + // Create database. + var dbContext = _requestExecutor.Services + .GetApplicationServices() + .GetRequiredService(); + + await dbContext.Database.EnsureCreatedAsync(); + } + + [Fact] + public async Task RegisterAttendee() + { + // Arrange & act + var result = await _requestExecutor.ExecuteAsync( + """ + mutation RegisterAttendee { + registerAttendee( + input: { + firstName: "Michael" + lastName: "Staib" + username: "michael" + emailAddress: "michael@chillicream.com" + } + ) { + attendee { + id + } + } + } + """, + TestContext.Current.CancellationToken); + + // Assert + result.MatchSnapshot(extension: ".json"); + } + + public async ValueTask DisposeAsync() + { + await _postgreSqlContainer.DisposeAsync(); + await _redisContainer.DisposeAsync(); + } +} diff --git a/code/session-7/GraphQL.Tests/GraphQL.Tests.csproj b/code/session-7/GraphQL.Tests/GraphQL.Tests.csproj new file mode 100644 index 0000000..a53f912 --- /dev/null +++ b/code/session-7/GraphQL.Tests/GraphQL.Tests.csproj @@ -0,0 +1,43 @@ + + + + enable + enable + Exe + GraphQL.Tests + net8.0 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/code/session-7/GraphQL.Tests/SchemaTests.cs b/code/session-7/GraphQL.Tests/SchemaTests.cs new file mode 100644 index 0000000..8978706 --- /dev/null +++ b/code/session-7/GraphQL.Tests/SchemaTests.cs @@ -0,0 +1,30 @@ +using ConferencePlanner.GraphQL.Data; +using CookieCrumble; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Execution; + +namespace GraphQL.Tests; + +public sealed class SchemaTests +{ + [Fact] + public async Task SchemaChanged() + { + // Arrange & act + var schema = await new ServiceCollection() + .AddDbContext() + .AddGraphQLServer() + .AddGlobalObjectIdentification() + .AddMutationConventions() + .AddDbContextCursorPagingProvider() + .AddPagingArguments() + .AddFiltering() + .AddSorting() + .AddInMemorySubscriptions() + .AddGraphQLTypes() + .BuildSchemaAsync(cancellationToken: TestContext.Current.CancellationToken); + + // Assert + schema.MatchSnapshot(extension: ".graphql"); + } +} diff --git a/code/session-8/GraphQL.Tests/__snapshots__/AttendeeTests.RegisterAttendee.snap b/code/session-7/GraphQL.Tests/__snapshots__/AttendeeTests.RegisterAttendee.json similarity index 67% rename from code/session-8/GraphQL.Tests/__snapshots__/AttendeeTests.RegisterAttendee.snap rename to code/session-7/GraphQL.Tests/__snapshots__/AttendeeTests.RegisterAttendee.json index 0e6d571..00e31de 100644 --- a/code/session-8/GraphQL.Tests/__snapshots__/AttendeeTests.RegisterAttendee.snap +++ b/code/session-7/GraphQL.Tests/__snapshots__/AttendeeTests.RegisterAttendee.json @@ -1,8 +1,8 @@ -{ +{ "data": { "registerAttendee": { "attendee": { - "id": "QXR0ZW5kZWUKaTE=" + "id": "QXR0ZW5kZWU6MQ==" } } } diff --git a/code/session-7/GraphQL.Tests/__snapshots__/SchemaTests.SchemaChanged.graphql b/code/session-7/GraphQL.Tests/__snapshots__/SchemaTests.SchemaChanged.graphql new file mode 100644 index 0000000..da2bb88 --- /dev/null +++ b/code/session-7/GraphQL.Tests/__snapshots__/SchemaTests.SchemaChanged.graphql @@ -0,0 +1,348 @@ +schema { + query: Query + mutation: Mutation + subscription: Subscription +} + +interface Error { + message: String! +} + +"The node interface is implemented by entities that have a global unique identifier." +interface Node { + id: ID! +} + +type AddSessionPayload { + session: Session + errors: [AddSessionError!] +} + +type AddSpeakerPayload { + speaker: Speaker +} + +type AddTrackPayload { + track: Track +} + +type Attendee implements Node { + sessions: [Session!]! @cost(weight: "10") + id: ID! + firstName: String! + lastName: String! + username: String! + emailAddress: String +} + +"A connection to a list of items." +type AttendeesConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [AttendeesEdge!] + "A flattened list of the nodes." + nodes: [Attendee!] +} + +"An edge in a connection." +type AttendeesEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Attendee! +} + +type CheckInAttendeePayload { + attendee: Attendee +} + +type EndTimeInvalidError implements Error { + message: String! +} + +type Mutation { + registerAttendee(input: RegisterAttendeeInput!): RegisterAttendeePayload! @cost(weight: "10") + checkInAttendee(input: CheckInAttendeeInput!): CheckInAttendeePayload! @cost(weight: "10") + addSession(input: AddSessionInput!): AddSessionPayload! @cost(weight: "10") + scheduleSession(input: ScheduleSessionInput!): ScheduleSessionPayload! @cost(weight: "10") + addSpeaker(input: AddSpeakerInput!): AddSpeakerPayload! @cost(weight: "10") + addTrack(input: AddTrackInput!): AddTrackPayload! @cost(weight: "10") + renameTrack(input: RenameTrackInput!): RenameTrackPayload! @cost(weight: "10") +} + +type NoSpeakerError implements Error { + message: String! +} + +"Information about pagination in a connection." +type PageInfo { + "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 + "When paginating forwards, the cursor to continue." + endCursor: String +} + +type Query { + "Fetches an object given its ID." + node("ID of the object." id: ID!): Node @cost(weight: "10") + "Lookup nodes by a list of IDs." + nodes("The list of node IDs." ids: [ID!]!): [Node]! @cost(weight: "10") + attendees("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): AttendeesConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + attendeeById(id: ID!): Attendee @cost(weight: "10") + attendeesById(ids: [ID!]!): [Attendee!]! @cost(weight: "10") + sessions("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: SessionFilterInput @cost(weight: "10") order: [SessionSortInput!] @cost(weight: "10")): SessionsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + sessionById(id: ID!): Session @cost(weight: "10") + sessionsById(ids: [ID!]!): [Session!]! @cost(weight: "10") + speakers("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): SpeakersConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + speakerById(id: ID!): Speaker @cost(weight: "10") + speakersById(ids: [ID!]!): [Speaker!]! @cost(weight: "10") + tracks("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): TracksConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + trackById(id: ID!): Track @cost(weight: "10") + tracksById(ids: [ID!]!): [Track!]! @cost(weight: "10") +} + +type RegisterAttendeePayload { + attendee: Attendee +} + +type RenameTrackPayload { + track: Track + errors: [RenameTrackError!] +} + +type ScheduleSessionPayload { + session: Session + errors: [ScheduleSessionError!] +} + +type Session implements Node { + duration: TimeSpan! + speakers: [Speaker!]! @cost(weight: "10") + attendees: [Attendee!]! @cost(weight: "10") + track: Track @cost(weight: "10") + trackId: ID + id: ID! + title: String! + abstract: String + startTime: DateTime + endTime: DateTime +} + +type SessionAttendeeCheckIn { + checkInCount: Int! @cost(weight: "10") + attendee: Attendee! @cost(weight: "10") + session: Session! @cost(weight: "10") + attendeeId: ID! + sessionId: ID! +} + +type SessionNotFoundError implements Error { + message: String! +} + +"A connection to a list of items." +type SessionsConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [SessionsEdge!] + "A flattened list of the nodes." + nodes: [Session!] +} + +"An edge in a connection." +type SessionsEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Session! +} + +type Speaker implements Node { + sessions: [Session!]! @cost(weight: "10") + id: ID! + name: String! + bio: String + website: String +} + +"A connection to a list of items." +type SpeakersConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [SpeakersEdge!] + "A flattened list of the nodes." + nodes: [Speaker!] +} + +"An edge in a connection." +type SpeakersEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Speaker! +} + +type Subscription { + onAttendeeCheckedIn(sessionId: ID!): SessionAttendeeCheckIn! @cost(weight: "10") + onSessionScheduled: Session! @cost(weight: "10") +} + +type TitleEmptyError implements Error { + message: String! +} + +type Track implements Node { + sessions("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): SessionsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") + name: String! + id: ID! +} + +type TrackNotFoundError implements Error { + message: String! +} + +"A connection to a list of items." +type TracksConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [TracksEdge!] + "A flattened list of the nodes." + nodes: [Track!] +} + +"An edge in a connection." +type TracksEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Track! +} + +union AddSessionError = TitleEmptyError | NoSpeakerError + +union RenameTrackError = TrackNotFoundError + +union ScheduleSessionError = EndTimeInvalidError | SessionNotFoundError + +input AddSessionInput { + title: String! + abstract: String + speakerIds: [ID!]! +} + +input AddSpeakerInput { + name: String! + bio: String + website: String +} + +input AddTrackInput { + name: String! +} + +input CheckInAttendeeInput { + sessionId: ID! + attendeeId: ID! +} + +input DateTimeOperationFilterInput { + eq: DateTime @cost(weight: "10") + neq: DateTime @cost(weight: "10") + in: [DateTime] @cost(weight: "10") + nin: [DateTime] @cost(weight: "10") + gt: DateTime @cost(weight: "10") + ngt: DateTime @cost(weight: "10") + gte: DateTime @cost(weight: "10") + ngte: DateTime @cost(weight: "10") + lt: DateTime @cost(weight: "10") + nlt: DateTime @cost(weight: "10") + lte: DateTime @cost(weight: "10") + nlte: DateTime @cost(weight: "10") +} + +input RegisterAttendeeInput { + firstName: String! + lastName: String! + username: String! + emailAddress: String! +} + +input RenameTrackInput { + id: ID! + name: String! +} + +input ScheduleSessionInput { + sessionId: ID! + trackId: ID! + startTime: DateTime! + endTime: DateTime! +} + +input SessionFilterInput { + and: [SessionFilterInput!] + or: [SessionFilterInput!] + title: StringOperationFilterInput + abstract: StringOperationFilterInput + startTime: DateTimeOperationFilterInput + endTime: DateTimeOperationFilterInput +} + +input SessionSortInput { + id: SortEnumType @cost(weight: "10") + title: SortEnumType @cost(weight: "10") + abstract: SortEnumType @cost(weight: "10") + startTime: SortEnumType @cost(weight: "10") + endTime: SortEnumType @cost(weight: "10") + duration: SortEnumType @cost(weight: "10") + trackId: SortEnumType @cost(weight: "10") + track: TrackSortInput @cost(weight: "10") +} + +input StringOperationFilterInput { + and: [StringOperationFilterInput!] + or: [StringOperationFilterInput!] + eq: String @cost(weight: "10") + neq: String @cost(weight: "10") + contains: String @cost(weight: "20") + ncontains: String @cost(weight: "20") + in: [String] @cost(weight: "10") + nin: [String] @cost(weight: "10") + startsWith: String @cost(weight: "20") + nstartsWith: String @cost(weight: "20") + endsWith: String @cost(weight: "20") + nendsWith: String @cost(weight: "20") +} + +input TrackSortInput { + id: SortEnumType @cost(weight: "10") + name: SortEnumType @cost(weight: "10") +} + +enum SortEnumType { + ASC + DESC +} + +"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response." +directive @cost("The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." weight: String!) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION + +"The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." +directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!] "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." slicingArgumentDefaultValue: Int "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `requireOneSlicingArgument` argument can be used to inform the static analysis that it should expect that exactly one of the defined slicing arguments is present in a query. If that is not the case (i.e., if none or multiple slicing arguments are present), the static analysis may throw an error." requireOneSlicingArgument: Boolean! = true) on FIELD_DEFINITION + +"The `@specifiedBy` directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar definitions." +directive @specifiedBy("The specifiedBy URL points to a human-readable specification. This field will only read a result for scalar types." url: String!) on SCALAR + +"The `DateTime` scalar represents an ISO-8601 compliant date time type." +scalar DateTime @specifiedBy(url: "https:\/\/www.graphql-scalars.com\/date-time") + +"The `TimeSpan` scalar represents an ISO-8601 compliant duration type." +scalar TimeSpan diff --git a/code/session-7/GraphQL.Tests/xunit.runner.json b/code/session-7/GraphQL.Tests/xunit.runner.json new file mode 100644 index 0000000..c2f8426 --- /dev/null +++ b/code/session-7/GraphQL.Tests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json" +} diff --git a/code/session-7/GraphQL/Attendees/AttendeeDataLoaders.cs b/code/session-7/GraphQL/Attendees/AttendeeDataLoaders.cs new file mode 100644 index 0000000..56b8e21 --- /dev/null +++ b/code/session-7/GraphQL/Attendees/AttendeeDataLoaders.cs @@ -0,0 +1,36 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Attendees; + +public static class AttendeeDataLoaders +{ + [DataLoader] + public static async Task> AttendeeByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Attendees + .AsNoTracking() + .Where(a => ids.Contains(a.Id)) + .Select(a => a.Id, selector) + .ToDictionaryAsync(a => a.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SessionsByAttendeeIdAsync( + IReadOnlyList attendeeIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Attendees + .AsNoTracking() + .Where(a => attendeeIds.Contains(a.Id)) + .Select(a => a.Id, a => a.SessionsAttendees.Select(sa => sa.Session), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-7/GraphQL/Attendees/AttendeeExceptions.cs b/code/session-7/GraphQL/Attendees/AttendeeExceptions.cs new file mode 100644 index 0000000..59fe765 --- /dev/null +++ b/code/session-7/GraphQL/Attendees/AttendeeExceptions.cs @@ -0,0 +1,3 @@ +namespace ConferencePlanner.GraphQL.Attendees; + +public sealed class AttendeeNotFoundException() : Exception("Attendee not found."); diff --git a/code/session-7/GraphQL/Attendees/AttendeeMutations.cs b/code/session-7/GraphQL/Attendees/AttendeeMutations.cs index 81d1e02..6496fed 100644 --- a/code/session-7/GraphQL/Attendees/AttendeeMutations.cs +++ b/code/session-7/GraphQL/Attendees/AttendeeMutations.cs @@ -1,68 +1,56 @@ -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Common; using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; using HotChocolate.Subscriptions; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Attendees; -namespace ConferencePlanner.GraphQL.Attendees +[MutationType] +public static class AttendeeMutations { - [ExtendObjectType(Name = "Mutation")] - public class AttendeeMutations + public static async Task RegisterAttendeeAsync( + RegisterAttendeeInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - [UseApplicationDbContext] - public async Task RegisterAttendeeAsync( - RegisterAttendeeInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) + var attendee = new Attendee { - var attendee = new Attendee - { - FirstName = input.FirstName, - LastName = input.LastName, - UserName = input.UserName, - EmailAddress = input.EmailAddress - }; + FirstName = input.FirstName, + LastName = input.LastName, + Username = input.Username, + EmailAddress = input.EmailAddress + }; - context.Attendees.Add(attendee); + dbContext.Attendees.Add(attendee); - await context.SaveChangesAsync(cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); - return new RegisterAttendeePayload(attendee); - } + return attendee; + } - [UseApplicationDbContext] - public async Task CheckInAttendeeAsync( - CheckInAttendeeInput input, - [ScopedService] ApplicationDbContext context, - [Service] ITopicEventSender eventSender, - CancellationToken cancellationToken) - { - Attendee attendee = await context.Attendees.FirstOrDefaultAsync( - t => t.Id == input.AttendeeId, cancellationToken); + public static async Task CheckInAttendeeAsync( + CheckInAttendeeInput input, + ApplicationDbContext dbContext, + ITopicEventSender eventSender, + CancellationToken cancellationToken) + { + var attendee = await dbContext.Attendees.FirstOrDefaultAsync( + a => a.Id == input.AttendeeId, + cancellationToken); - if (attendee is null) - { - return new CheckInAttendeePayload( - new UserError("Attendee not found.", "ATTENDEE_NOT_FOUND")); - } + if (attendee is null) + { + throw new AttendeeNotFoundException(); + } - attendee.SessionsAttendees.Add( - new SessionAttendee - { - SessionId = input.SessionId - }); + attendee.SessionsAttendees.Add(new SessionAttendee { SessionId = input.SessionId }); - await context.SaveChangesAsync(cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); - await eventSender.SendAsync( - "OnAttendeeCheckedIn_" + input.SessionId, - input.AttendeeId, - cancellationToken); + await eventSender.SendAsync( + $"OnAttendeeCheckedIn_{input.SessionId}", + input.AttendeeId, + cancellationToken); - return new CheckInAttendeePayload(attendee, input.SessionId); - } + return attendee; } -} \ No newline at end of file +} diff --git a/code/session-7/GraphQL/Attendees/AttendeePayloadBase.cs b/code/session-7/GraphQL/Attendees/AttendeePayloadBase.cs deleted file mode 100644 index 4b98558..0000000 --- a/code/session-7/GraphQL/Attendees/AttendeePayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Attendees -{ - public class AttendeePayloadBase : Payload - { - protected AttendeePayloadBase(Attendee attendee) - { - Attendee = attendee; - } - - protected AttendeePayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Attendee? Attendee { get; } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Attendees/AttendeeQueries.cs b/code/session-7/GraphQL/Attendees/AttendeeQueries.cs index e877335..bda229c 100644 --- a/code/session-7/GraphQL/Attendees/AttendeeQueries.cs +++ b/code/session-7/GraphQL/Attendees/AttendeeQueries.cs @@ -1,34 +1,35 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; -namespace ConferencePlanner.GraphQL.Attendees +namespace ConferencePlanner.GraphQL.Attendees; + +[QueryType] +public static class AttendeeQueries { - [ExtendObjectType(Name = "Query")] - public class AttendeeQueries + [UsePaging] + public static IQueryable GetAttendees(ApplicationDbContext dbContext) { - [UseApplicationDbContext] - [UsePaging] - public IQueryable GetAttendees( - [ScopedService] ApplicationDbContext context) => - context.Attendees; + return dbContext.Attendees.AsNoTracking().OrderBy(a => a.Username); + } - public Task GetAttendeeByIdAsync( - [ID(nameof(Attendee))] int id, - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) => - attendeeById.LoadAsync(id, cancellationToken); + [NodeResolver] + public static async Task GetAttendeeByIdAsync( + int id, + IAttendeeByIdDataLoader attendeeById, + ISelection selection, + CancellationToken cancellationToken) + { + return await attendeeById.Select(selection).LoadAsync(id, cancellationToken); + } - public async Task> GetAttendeesByIdAsync( - [ID(nameof(Attendee))] int[] ids, - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) => - await attendeeById.LoadAsync(ids, cancellationToken); + public static async Task> GetAttendeesByIdAsync( + [ID] int[] ids, + IAttendeeByIdDataLoader attendeeById, + ISelection selection, + CancellationToken cancellationToken) + { + return await attendeeById.Select(selection).LoadRequiredAsync(ids, cancellationToken); } -} \ No newline at end of file +} diff --git a/code/session-7/GraphQL/Attendees/AttendeeSubscriptions.cs b/code/session-7/GraphQL/Attendees/AttendeeSubscriptions.cs index 475098e..5d35833 100644 --- a/code/session-7/GraphQL/Attendees/AttendeeSubscriptions.cs +++ b/code/session-7/GraphQL/Attendees/AttendeeSubscriptions.cs @@ -1,31 +1,27 @@ -using System.Threading; -using System.Threading.Tasks; using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; using HotChocolate.Execution; using HotChocolate.Subscriptions; -using HotChocolate.Types; -using HotChocolate.Types.Relay; -namespace ConferencePlanner.GraphQL.Attendees +namespace ConferencePlanner.GraphQL.Attendees; + +[SubscriptionType] +public static class AttendeeSubscriptions { - [ExtendObjectType(Name = "Subscription")] - public class AttendeeSubscriptions + [Subscribe(With = nameof(SubscribeToOnAttendeeCheckedInAsync))] + public static SessionAttendeeCheckIn OnAttendeeCheckedIn( + [ID] int sessionId, + [EventMessage] int attendeeId) { - [Subscribe(With = nameof(SubscribeToOnAttendeeCheckedInAsync))] - public SessionAttendeeCheckIn OnAttendeeCheckedIn( - [ID(nameof(Session))] int sessionId, - [EventMessage] int attendeeId, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - new SessionAttendeeCheckIn(attendeeId, sessionId); + return new SessionAttendeeCheckIn(attendeeId, sessionId); + } - public async ValueTask> SubscribeToOnAttendeeCheckedInAsync( - int sessionId, - [Service] ITopicEventReceiver eventReceiver, - CancellationToken cancellationToken) => - await eventReceiver.SubscribeAsync( - "OnAttendeeCheckedIn_" + sessionId, cancellationToken); + public static async ValueTask> SubscribeToOnAttendeeCheckedInAsync( + int sessionId, + ITopicEventReceiver eventReceiver, + CancellationToken cancellationToken) + { + return await eventReceiver.SubscribeAsync( + $"OnAttendeeCheckedIn_{sessionId}", + cancellationToken); } -} \ No newline at end of file +} diff --git a/code/session-7/GraphQL/Attendees/AttendeeType.cs b/code/session-7/GraphQL/Attendees/AttendeeType.cs new file mode 100644 index 0000000..a76e76a --- /dev/null +++ b/code/session-7/GraphQL/Attendees/AttendeeType.cs @@ -0,0 +1,32 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Attendees; + +[ObjectType] +public static partial class AttendeeType +{ + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .ImplementsNode() + .IdField(a => a.Id) + .ResolveNode( + async (ctx, id) + => await ctx.DataLoader() + .LoadAsync(id, ctx.RequestAborted)); + } + + [BindMember(nameof(Attendee.SessionsAttendees))] + public static async Task> GetSessionsAsync( + [Parent] Attendee attendee, + ISessionsByAttendeeIdDataLoader sessionsByAttendeeId, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsByAttendeeId + .Select(selection) + .LoadRequiredAsync(attendee.Id, cancellationToken); + } +} diff --git a/code/session-7/GraphQL/Attendees/CheckInAttendeeInput.cs b/code/session-7/GraphQL/Attendees/CheckInAttendeeInput.cs index 5464c22..8f52b73 100644 --- a/code/session-7/GraphQL/Attendees/CheckInAttendeeInput.cs +++ b/code/session-7/GraphQL/Attendees/CheckInAttendeeInput.cs @@ -1,11 +1,7 @@ using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; -namespace ConferencePlanner.GraphQL.Attendees -{ - public record CheckInAttendeeInput( - [ID(nameof(Session))] - int SessionId, - [ID(nameof(Attendee))] - int AttendeeId); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Attendees; + +public sealed record CheckInAttendeeInput( + [property: ID] int SessionId, + [property: ID] int AttendeeId); diff --git a/code/session-7/GraphQL/Attendees/CheckInAttendeePayload.cs b/code/session-7/GraphQL/Attendees/CheckInAttendeePayload.cs deleted file mode 100644 index 29e1276..0000000 --- a/code/session-7/GraphQL/Attendees/CheckInAttendeePayload.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; - -namespace ConferencePlanner.GraphQL.Attendees -{ - public class CheckInAttendeePayload : AttendeePayloadBase - { - private int? _sessionId; - - public CheckInAttendeePayload(Attendee attendee, int sessionId) - : base(attendee) - { - _sessionId = sessionId; - } - - public CheckInAttendeePayload(UserError error) - : base(new[] { error }) - { - } - - public async Task GetSessionAsync( - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - if (_sessionId.HasValue) - { - return await sessionById.LoadAsync(_sessionId.Value, cancellationToken); - } - - return null; - } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Attendees/RegisterAttendeeInput.cs b/code/session-7/GraphQL/Attendees/RegisterAttendeeInput.cs index 1710f0b..4b40751 100644 --- a/code/session-7/GraphQL/Attendees/RegisterAttendeeInput.cs +++ b/code/session-7/GraphQL/Attendees/RegisterAttendeeInput.cs @@ -1,8 +1,7 @@ -namespace ConferencePlanner.GraphQL.Attendees -{ - public record RegisterAttendeeInput( - string FirstName, - string LastName, - string UserName, - string EmailAddress); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Attendees; + +public sealed record RegisterAttendeeInput( + string FirstName, + string LastName, + string Username, + string EmailAddress); diff --git a/code/session-7/GraphQL/Attendees/RegisterAttendeePayload.cs b/code/session-7/GraphQL/Attendees/RegisterAttendeePayload.cs deleted file mode 100644 index a79e99a..0000000 --- a/code/session-7/GraphQL/Attendees/RegisterAttendeePayload.cs +++ /dev/null @@ -1,18 +0,0 @@ -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Attendees -{ - public class RegisterAttendeePayload : AttendeePayloadBase - { - public RegisterAttendeePayload(Attendee attendee) - : base(attendee) - { - } - - public RegisterAttendeePayload(UserError error) - : base(new[] { error }) - { - } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Attendees/SessionAttendeeCheckIn.cs b/code/session-7/GraphQL/Attendees/SessionAttendeeCheckIn.cs index af3c161..fa6bacf 100644 --- a/code/session-7/GraphQL/Attendees/SessionAttendeeCheckIn.cs +++ b/code/session-7/GraphQL/Attendees/SessionAttendeeCheckIn.cs @@ -1,45 +1,39 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types.Relay; +using ConferencePlanner.GraphQL.Sessions; -namespace ConferencePlanner.GraphQL.Attendees -{ - public class SessionAttendeeCheckIn - { - public SessionAttendeeCheckIn(int attendeeId, int sessionId) - { - AttendeeId = attendeeId; - SessionId = sessionId; - } +namespace ConferencePlanner.GraphQL.Attendees; - [ID(nameof(Attendee))] - public int AttendeeId { get; } +public sealed class SessionAttendeeCheckIn(int attendeeId, int sessionId) +{ + [ID] + public int AttendeeId { get; } = attendeeId; - [ID(nameof(Session))] - public int SessionId { get; } + [ID] + public int SessionId { get; } = sessionId; - [UseApplicationDbContext] - public async Task CheckInCountAsync( - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - await context.Sessions - .Where(session => session.Id == SessionId) - .SelectMany(session => session.SessionAttendees) - .CountAsync(cancellationToken); + public async Task CheckInCountAsync( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => s.Id == SessionId) + .SelectMany(s => s.SessionAttendees) + .CountAsync(cancellationToken); + } - public Task GetAttendeeAsync( - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) => - attendeeById.LoadAsync(AttendeeId, cancellationToken); + public async Task GetAttendeeAsync( + IAttendeeByIdDataLoader attendeeById, + CancellationToken cancellationToken) + { + return await attendeeById.LoadRequiredAsync(AttendeeId, cancellationToken); + } - public Task GetSessionAsync( - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - sessionById.LoadAsync(AttendeeId, cancellationToken); + public async Task GetSessionAsync( + ISessionByIdDataLoader sessionById, + CancellationToken cancellationToken) + { + return await sessionById.LoadRequiredAsync(SessionId, cancellationToken); } -} \ No newline at end of file +} diff --git a/code/session-7/GraphQL/Common/Payload.cs b/code/session-7/GraphQL/Common/Payload.cs deleted file mode 100644 index e9d2839..0000000 --- a/code/session-7/GraphQL/Common/Payload.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace ConferencePlanner.GraphQL.Common -{ - public abstract class Payload - { - protected Payload(IReadOnlyList? errors = null) - { - Errors = errors; - } - - public IReadOnlyList? Errors { get; } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Common/UserError.cs b/code/session-7/GraphQL/Common/UserError.cs deleted file mode 100644 index 3d587dd..0000000 --- a/code/session-7/GraphQL/Common/UserError.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace ConferencePlanner.GraphQL.Common -{ - public class UserError - { - public UserError(string message, string code) - { - Message = message; - Code = code; - } - - public string Message { get; } - - public string Code { get; } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Data/ApplicationDbContext.cs b/code/session-7/GraphQL/Data/ApplicationDbContext.cs index bbd7dda..5a2d633 100644 --- a/code/session-7/GraphQL/Data/ApplicationDbContext.cs +++ b/code/session-7/GraphQL/Data/ApplicationDbContext.cs @@ -1,38 +1,33 @@ - using Microsoft.EntityFrameworkCore; - - namespace ConferencePlanner.GraphQL.Data - { - public class ApplicationDbContext : DbContext - { - public ApplicationDbContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasIndex(a => a.UserName) - .IsUnique(); - - // Many-to-many: Session <-> Attendee - modelBuilder - .Entity() - .HasKey(ca => new { ca.SessionId, ca.AttendeeId }); - - // Many-to-many: Speaker <-> Session - modelBuilder - .Entity() - .HasKey(ss => new { ss.SessionId, ss.SpeakerId }); - } - - public DbSet Sessions { get; set; } = default!; - - public DbSet Tracks { get; set; } = default!; - - public DbSet Speakers { get; set; } = default!; - - public DbSet Attendees { get; set; } = default!; - } - } \ No newline at end of file +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Data; + +public sealed class ApplicationDbContext(DbContextOptions options) + : DbContext(options) +{ + public DbSet Attendees { get; init; } + + public DbSet Sessions { get; init; } + + public DbSet Speakers { get; init; } + + public DbSet Tracks { get; init; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .HasIndex(a => a.Username) + .IsUnique(); + + // Many-to-many: Session <-> Attendee + modelBuilder + .Entity() + .HasKey(sa => new { sa.SessionId, sa.AttendeeId }); + + // Many-to-many: Speaker <-> Session + modelBuilder + .Entity() + .HasKey(ss => new { ss.SessionId, ss.SpeakerId }); + } +} diff --git a/code/session-7/GraphQL/Data/Attendee.cs b/code/session-7/GraphQL/Data/Attendee.cs index e3f9ab0..304ec00 100644 --- a/code/session-7/GraphQL/Data/Attendee.cs +++ b/code/session-7/GraphQL/Data/Attendee.cs @@ -1,28 +1,23 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Attendee { - public class Attendee - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? FirstName { get; set; } + [StringLength(200)] + public required string FirstName { get; init; } - [Required] - [StringLength(200)] - public string? LastName { get; set; } + [StringLength(200)] + public required string LastName { get; init; } - [Required] - [StringLength(200)] - public string? UserName { get; set; } + [StringLength(200)] + public required string Username { get; init; } - [StringLength(256)] - public string? EmailAddress { get; set; } + [StringLength(256)] + public string? EmailAddress { get; init; } - public ICollection SessionsAttendees { get; set; } = - new List(); - } -} \ No newline at end of file + public ICollection SessionsAttendees { get; init; } = + new List(); +} diff --git a/code/session-7/GraphQL/Data/Session.cs b/code/session-7/GraphQL/Data/Session.cs index b340977..086d57a 100644 --- a/code/session-7/GraphQL/Data/Session.cs +++ b/code/session-7/GraphQL/Data/Session.cs @@ -1,37 +1,33 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Session { - public class Session - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? Title { get; set; } + [StringLength(200)] + public required string Title { get; init; } - [StringLength(4000)] - public string? Abstract { get; set; } + [StringLength(4000)] + public string? Abstract { get; init; } - public DateTimeOffset? StartTime { get; set; } + public DateTimeOffset? StartTime { get; set; } - public DateTimeOffset? EndTime { get; set; } + public DateTimeOffset? EndTime { get; set; } - // Bonus points to those who can figure out why this is written this way - public TimeSpan Duration => - EndTime?.Subtract(StartTime ?? EndTime ?? DateTimeOffset.MinValue) ?? - TimeSpan.Zero; + // Bonus points to those who can figure out why this is written this way. + public TimeSpan Duration => + EndTime?.Subtract(StartTime ?? EndTime ?? DateTimeOffset.MinValue) ?? + TimeSpan.Zero; - public int? TrackId { get; set; } + public int? TrackId { get; set; } - public ICollection SessionSpeakers { get; set; } = - new List(); + public ICollection SessionSpeakers { get; init; } = + new List(); - public ICollection SessionAttendees { get; set; } = - new List(); + public ICollection SessionAttendees { get; init; } = + new List(); - public Track? Track { get; set; } - } -} \ No newline at end of file + public Track? Track { get; init; } +} diff --git a/code/session-7/GraphQL/Data/SessionAttendee.cs b/code/session-7/GraphQL/Data/SessionAttendee.cs index 089c71a..892d5ae 100644 --- a/code/session-7/GraphQL/Data/SessionAttendee.cs +++ b/code/session-7/GraphQL/Data/SessionAttendee.cs @@ -1,13 +1,12 @@ -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class SessionAttendee { - public class SessionAttendee - { - public int SessionId { get; set; } + public int SessionId { get; init; } - public Session? Session { get; set; } + public Session Session { get; init; } = null!; - public int AttendeeId { get; set; } + public int AttendeeId { get; init; } - public Attendee? Attendee { get; set; } - } -} \ No newline at end of file + public Attendee Attendee { get; init; } = null!; +} diff --git a/code/session-7/GraphQL/Data/SessionSpeaker.cs b/code/session-7/GraphQL/Data/SessionSpeaker.cs index ed83e86..aeebe62 100644 --- a/code/session-7/GraphQL/Data/SessionSpeaker.cs +++ b/code/session-7/GraphQL/Data/SessionSpeaker.cs @@ -1,13 +1,12 @@ -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class SessionSpeaker { - public class SessionSpeaker - { - public int SessionId { get; set; } + public int SessionId { get; init; } - public Session? Session { get; set; } + public Session Session { get; init; } = null!; - public int SpeakerId { get; set; } + public int SpeakerId { get; init; } - public Speaker? Speaker { get; set; } - } -} \ No newline at end of file + public Speaker Speaker { get; init; } = null!; +} diff --git a/code/session-7/GraphQL/Data/Speaker.cs b/code/session-7/GraphQL/Data/Speaker.cs index 0943514..bf47876 100644 --- a/code/session-7/GraphQL/Data/Speaker.cs +++ b/code/session-7/GraphQL/Data/Speaker.cs @@ -1,23 +1,20 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Speaker { - public class Speaker - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? Name { get; set; } + [StringLength(200)] + public required string Name { get; init; } - [StringLength(4000)] - public string? Bio { get; set; } + [StringLength(4000)] + public string? Bio { get; init; } - [StringLength(1000)] - public string? WebSite { get; set; } + [StringLength(1000)] + public string? Website { get; init; } - public ICollection SessionSpeakers { get; set; } = - new List(); - } - } \ No newline at end of file + public ICollection SessionSpeakers { get; init; } = + new List(); +} diff --git a/code/session-7/GraphQL/Data/Track.cs b/code/session-7/GraphQL/Data/Track.cs index f2392b6..51bd27b 100644 --- a/code/session-7/GraphQL/Data/Track.cs +++ b/code/session-7/GraphQL/Data/Track.cs @@ -1,17 +1,14 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ConferencePlanner.GraphQL.Data +namespace ConferencePlanner.GraphQL.Data; + +public sealed class Track { - public class Track - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string? Name { get; set; } + [StringLength(200)] + public required string Name { get; set; } - public ICollection Sessions { get; set; } = - new List(); - } -} \ No newline at end of file + public ICollection Sessions { get; init; } = + new List(); +} diff --git a/code/session-7/GraphQL/DataLoader/AttendeeByIdDataLoader.cs b/code/session-7/GraphQL/DataLoader/AttendeeByIdDataLoader.cs deleted file mode 100644 index a2bdbd0..0000000 --- a/code/session-7/GraphQL/DataLoader/AttendeeByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class AttendeeByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public AttendeeByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Attendees - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/DataLoader/SessionByIdDataLoader.cs b/code/session-7/GraphQL/DataLoader/SessionByIdDataLoader.cs deleted file mode 100644 index dbd675b..0000000 --- a/code/session-7/GraphQL/DataLoader/SessionByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class SessionByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public SessionByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Sessions - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/DataLoader/SpeakerByIdDataLoader.cs b/code/session-7/GraphQL/DataLoader/SpeakerByIdDataLoader.cs deleted file mode 100644 index 44d8208..0000000 --- a/code/session-7/GraphQL/DataLoader/SpeakerByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class SpeakerByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public SpeakerByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Speakers - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/DataLoader/TrackByIdDataLoader.cs b/code/session-7/GraphQL/DataLoader/TrackByIdDataLoader.cs deleted file mode 100644 index 4db1f95..0000000 --- a/code/session-7/GraphQL/DataLoader/TrackByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class TrackByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public TrackByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Tracks - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs b/code/session-7/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs index 370c767..49c13a8 100644 --- a/code/session-7/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs +++ b/code/session-7/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs @@ -1,32 +1,17 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using HotChocolate.Types; +namespace ConferencePlanner.GraphQL.Extensions; -namespace ConferencePlanner.GraphQL +public static class ObjectFieldDescriptorExtensions { - public static class ObjectFieldDescriptorExtensions + public static IObjectFieldDescriptor UseUpperCase(this IObjectFieldDescriptor descriptor) { - public static IObjectFieldDescriptor UseDbContext( - this IObjectFieldDescriptor descriptor) - where TDbContext : DbContext + return descriptor.Use(next => async context => { - return descriptor.UseScopedService( - create: s => s.GetRequiredService>().CreateDbContext(), - disposeAsync: (s, c) => c.DisposeAsync()); - } + await next(context); - public static IObjectFieldDescriptor UseUpperCase( - this IObjectFieldDescriptor descriptor) - { - return descriptor.Use(next => async context => + if (context.Result is string s) { - await next(context); - - if (context.Result is string s) - { - context.Result = s.ToUpperInvariant(); - } - }); - } + context.Result = s.ToUpperInvariant(); + } + }); } -} \ No newline at end of file +} diff --git a/code/session-7/GraphQL/Extensions/UseApplicationDbContextAttribute.cs b/code/session-7/GraphQL/Extensions/UseApplicationDbContextAttribute.cs deleted file mode 100644 index 79c9907..0000000 --- a/code/session-7/GraphQL/Extensions/UseApplicationDbContextAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Reflection; -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types; -using HotChocolate.Types.Descriptors; - -namespace ConferencePlanner.GraphQL -{ - public class UseApplicationDbContextAttribute : ObjectFieldDescriptorAttribute - { - public override void OnConfigure( - IDescriptorContext context, - IObjectFieldDescriptor descriptor, - MemberInfo member) - { - descriptor.UseDbContext(); - } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Extensions/UseUpperCaseAttribute.cs b/code/session-7/GraphQL/Extensions/UseUpperCaseAttribute.cs index 8376b5b..b85152d 100644 --- a/code/session-7/GraphQL/Extensions/UseUpperCaseAttribute.cs +++ b/code/session-7/GraphQL/Extensions/UseUpperCaseAttribute.cs @@ -1,17 +1,15 @@ -using HotChocolate.Types; -using HotChocolate.Types.Descriptors; using System.Reflection; +using HotChocolate.Types.Descriptors; + +namespace ConferencePlanner.GraphQL.Extensions; -namespace ConferencePlanner.GraphQL +public sealed class UseUpperCaseAttribute : ObjectFieldDescriptorAttribute { - public class UseUpperCaseAttribute : ObjectFieldDescriptorAttribute + protected override void OnConfigure( + IDescriptorContext context, + IObjectFieldDescriptor descriptor, + MemberInfo member) { - public override void OnConfigure( - IDescriptorContext context, - IObjectFieldDescriptor descriptor, - MemberInfo member) - { - descriptor.UseUpperCase(); - } + descriptor.UseUpperCase(); } -} \ No newline at end of file +} diff --git a/code/session-7/GraphQL/GraphQL.csproj b/code/session-7/GraphQL/GraphQL.csproj index 0a3a4a0..f079203 100644 --- a/code/session-7/GraphQL/GraphQL.csproj +++ b/code/session-7/GraphQL/GraphQL.csproj @@ -1,19 +1,25 @@ - - - - net5.0 - ConferencePlanner.GraphQL - enable - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - + + + + net8.0 + enable + enable + ConferencePlanner.GraphQL + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/code/session-7/GraphQL/Migrations/20201010183502_Initial.Designer.cs b/code/session-7/GraphQL/Migrations/20201010183502_Initial.Designer.cs deleted file mode 100644 index dc170c1..0000000 --- a/code/session-7/GraphQL/Migrations/20201010183502_Initial.Designer.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -using ConferencePlanner.GraphQL.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace GraphQL.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20201010183502_Initial")] - partial class Initial - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bio") - .HasMaxLength(4000) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("WebSite") - .HasMaxLength(1000) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Speakers"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/code/session-7/GraphQL/Migrations/20201010183502_Initial.cs b/code/session-7/GraphQL/Migrations/20201010183502_Initial.cs deleted file mode 100644 index 69e30fb..0000000 --- a/code/session-7/GraphQL/Migrations/20201010183502_Initial.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace GraphQL.Migrations -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Speakers", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), - Bio = table.Column(type: "TEXT", maxLength: 4000, nullable: true), - WebSite = table.Column(type: "TEXT", maxLength: 1000, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Speakers", x => x.Id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Speakers"); - } - } -} diff --git a/code/session-7/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs b/code/session-7/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs deleted file mode 100644 index 2e3d723..0000000 --- a/code/session-7/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs +++ /dev/null @@ -1,226 +0,0 @@ -// -using System; -using ConferencePlanner.GraphQL.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace GraphQL.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20201010202211_Refactoring")] - partial class Refactoring - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("EmailAddress") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("FirstName") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("LastName") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserName") - .IsUnique(); - - b.ToTable("Attendees"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Abstract") - .HasMaxLength(4000) - .HasColumnType("TEXT"); - - b.Property("EndTime") - .HasColumnType("TEXT"); - - b.Property("StartTime") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("TrackId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TrackId"); - - b.ToTable("Sessions"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => - { - b.Property("SessionId") - .HasColumnType("INTEGER"); - - b.Property("AttendeeId") - .HasColumnType("INTEGER"); - - b.HasKey("SessionId", "AttendeeId"); - - b.HasIndex("AttendeeId"); - - b.ToTable("SessionAttendee"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => - { - b.Property("SessionId") - .HasColumnType("INTEGER"); - - b.Property("SpeakerId") - .HasColumnType("INTEGER"); - - b.HasKey("SessionId", "SpeakerId"); - - b.HasIndex("SpeakerId"); - - b.ToTable("SessionSpeaker"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bio") - .HasMaxLength(4000) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("WebSite") - .HasMaxLength(1000) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Speakers"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Track", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Tracks"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => - { - b.HasOne("ConferencePlanner.GraphQL.Data.Track", "Track") - .WithMany("Sessions") - .HasForeignKey("TrackId"); - - b.Navigation("Track"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => - { - b.HasOne("ConferencePlanner.GraphQL.Data.Attendee", "Attendee") - .WithMany("SessionsAttendees") - .HasForeignKey("AttendeeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("ConferencePlanner.GraphQL.Data.Session", "Session") - .WithMany("SessionAttendees") - .HasForeignKey("SessionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Attendee"); - - b.Navigation("Session"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => - { - b.HasOne("ConferencePlanner.GraphQL.Data.Session", "Session") - .WithMany("SessionSpeakers") - .HasForeignKey("SessionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("ConferencePlanner.GraphQL.Data.Speaker", "Speaker") - .WithMany("SessionSpeakers") - .HasForeignKey("SpeakerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Session"); - - b.Navigation("Speaker"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => - { - b.Navigation("SessionsAttendees"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => - { - b.Navigation("SessionAttendees"); - - b.Navigation("SessionSpeakers"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => - { - b.Navigation("SessionSpeakers"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Track", b => - { - b.Navigation("Sessions"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/code/session-7/GraphQL/Migrations/20201010202211_Refactoring.cs b/code/session-7/GraphQL/Migrations/20201010202211_Refactoring.cs deleted file mode 100644 index ffdcfeb..0000000 --- a/code/session-7/GraphQL/Migrations/20201010202211_Refactoring.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace GraphQL.Migrations -{ - public partial class Refactoring : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Attendees", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - FirstName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - LastName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - UserName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - EmailAddress = table.Column(type: "TEXT", maxLength: 256, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Attendees", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Tracks", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", maxLength: 200, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Tracks", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Sessions", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Title = table.Column(type: "TEXT", maxLength: 200, nullable: false), - Abstract = table.Column(type: "TEXT", maxLength: 4000, nullable: true), - StartTime = table.Column(type: "TEXT", nullable: true), - EndTime = table.Column(type: "TEXT", nullable: true), - TrackId = table.Column(type: "INTEGER", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Sessions", x => x.Id); - table.ForeignKey( - name: "FK_Sessions_Tracks_TrackId", - column: x => x.TrackId, - principalTable: "Tracks", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "SessionAttendee", - columns: table => new - { - SessionId = table.Column(type: "INTEGER", nullable: false), - AttendeeId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_SessionAttendee", x => new { x.SessionId, x.AttendeeId }); - table.ForeignKey( - name: "FK_SessionAttendee_Attendees_AttendeeId", - column: x => x.AttendeeId, - principalTable: "Attendees", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_SessionAttendee_Sessions_SessionId", - column: x => x.SessionId, - principalTable: "Sessions", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "SessionSpeaker", - columns: table => new - { - SessionId = table.Column(type: "INTEGER", nullable: false), - SpeakerId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_SessionSpeaker", x => new { x.SessionId, x.SpeakerId }); - table.ForeignKey( - name: "FK_SessionSpeaker_Sessions_SessionId", - column: x => x.SessionId, - principalTable: "Sessions", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_SessionSpeaker_Speakers_SpeakerId", - column: x => x.SpeakerId, - principalTable: "Speakers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Attendees_UserName", - table: "Attendees", - column: "UserName", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_SessionAttendee_AttendeeId", - table: "SessionAttendee", - column: "AttendeeId"); - - migrationBuilder.CreateIndex( - name: "IX_Sessions_TrackId", - table: "Sessions", - column: "TrackId"); - - migrationBuilder.CreateIndex( - name: "IX_SessionSpeaker_SpeakerId", - table: "SessionSpeaker", - column: "SpeakerId"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "SessionAttendee"); - - migrationBuilder.DropTable( - name: "SessionSpeaker"); - - migrationBuilder.DropTable( - name: "Attendees"); - - migrationBuilder.DropTable( - name: "Sessions"); - - migrationBuilder.DropTable( - name: "Tracks"); - } - } -} diff --git a/code/session-7/GraphQL/Migrations/20240807140835_Initial.Designer.cs b/code/session-7/GraphQL/Migrations/20240807140835_Initial.Designer.cs new file mode 100644 index 0000000..d508064 --- /dev/null +++ b/code/session-7/GraphQL/Migrations/20240807140835_Initial.Designer.cs @@ -0,0 +1,55 @@ +// +using ConferencePlanner.GraphQL.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240807140835_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Bio") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Website") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.HasKey("Id"); + + b.ToTable("Speakers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/code/session-7/GraphQL/Migrations/20240807140835_Initial.cs b/code/session-7/GraphQL/Migrations/20240807140835_Initial.cs new file mode 100644 index 0000000..7301c7a --- /dev/null +++ b/code/session-7/GraphQL/Migrations/20240807140835_Initial.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Speakers", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Bio = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + Website = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Speakers", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Speakers"); + } + } +} diff --git a/code/session-7/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs b/code/session-7/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs new file mode 100644 index 0000000..75c788f --- /dev/null +++ b/code/session-7/GraphQL/Migrations/20240812080119_Refactoring.Designer.cs @@ -0,0 +1,241 @@ +// +using System; +using ConferencePlanner.GraphQL.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240812080119_Refactoring")] + partial class Refactoring + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EmailAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Attendees"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Abstract") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TrackId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.ToTable("Sessions"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => + { + b.Property("SessionId") + .HasColumnType("integer"); + + b.Property("AttendeeId") + .HasColumnType("integer"); + + b.HasKey("SessionId", "AttendeeId"); + + b.HasIndex("AttendeeId"); + + b.ToTable("SessionAttendee"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => + { + b.Property("SessionId") + .HasColumnType("integer"); + + b.Property("SpeakerId") + .HasColumnType("integer"); + + b.HasKey("SessionId", "SpeakerId"); + + b.HasIndex("SpeakerId"); + + b.ToTable("SessionSpeaker"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Bio") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Website") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.HasKey("Id"); + + b.ToTable("Speakers"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => + { + b.HasOne("ConferencePlanner.GraphQL.Data.Track", "Track") + .WithMany("Sessions") + .HasForeignKey("TrackId"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => + { + b.HasOne("ConferencePlanner.GraphQL.Data.Attendee", "Attendee") + .WithMany("SessionsAttendees") + .HasForeignKey("AttendeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ConferencePlanner.GraphQL.Data.Session", "Session") + .WithMany("SessionAttendees") + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Attendee"); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => + { + b.HasOne("ConferencePlanner.GraphQL.Data.Session", "Session") + .WithMany("SessionSpeakers") + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ConferencePlanner.GraphQL.Data.Speaker", "Speaker") + .WithMany("SessionSpeakers") + .HasForeignKey("SpeakerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Session"); + + b.Navigation("Speaker"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => + { + b.Navigation("SessionsAttendees"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => + { + b.Navigation("SessionAttendees"); + + b.Navigation("SessionSpeakers"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => + { + b.Navigation("SessionSpeakers"); + }); + + modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Track", b => + { + b.Navigation("Sessions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/code/session-7/GraphQL/Migrations/20240812080119_Refactoring.cs b/code/session-7/GraphQL/Migrations/20240812080119_Refactoring.cs new file mode 100644 index 0000000..e544f2e --- /dev/null +++ b/code/session-7/GraphQL/Migrations/20240812080119_Refactoring.cs @@ -0,0 +1,155 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations +{ + /// + public partial class Refactoring : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Attendees", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + FirstName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + LastName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Username = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + EmailAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Attendees", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Tracks", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tracks", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Sessions", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Abstract = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true), + StartTime = table.Column(type: "timestamp with time zone", nullable: true), + EndTime = table.Column(type: "timestamp with time zone", nullable: true), + TrackId = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Sessions", x => x.Id); + table.ForeignKey( + name: "FK_Sessions_Tracks_TrackId", + column: x => x.TrackId, + principalTable: "Tracks", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "SessionAttendee", + columns: table => new + { + SessionId = table.Column(type: "integer", nullable: false), + AttendeeId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SessionAttendee", x => new { x.SessionId, x.AttendeeId }); + table.ForeignKey( + name: "FK_SessionAttendee_Attendees_AttendeeId", + column: x => x.AttendeeId, + principalTable: "Attendees", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_SessionAttendee_Sessions_SessionId", + column: x => x.SessionId, + principalTable: "Sessions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "SessionSpeaker", + columns: table => new + { + SessionId = table.Column(type: "integer", nullable: false), + SpeakerId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SessionSpeaker", x => new { x.SessionId, x.SpeakerId }); + table.ForeignKey( + name: "FK_SessionSpeaker_Sessions_SessionId", + column: x => x.SessionId, + principalTable: "Sessions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_SessionSpeaker_Speakers_SpeakerId", + column: x => x.SpeakerId, + principalTable: "Speakers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Attendees_Username", + table: "Attendees", + column: "Username", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_SessionAttendee_AttendeeId", + table: "SessionAttendee", + column: "AttendeeId"); + + migrationBuilder.CreateIndex( + name: "IX_Sessions_TrackId", + table: "Sessions", + column: "TrackId"); + + migrationBuilder.CreateIndex( + name: "IX_SessionSpeaker_SpeakerId", + table: "SessionSpeaker", + column: "SpeakerId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SessionAttendee"); + + migrationBuilder.DropTable( + name: "SessionSpeaker"); + + migrationBuilder.DropTable( + name: "Attendees"); + + migrationBuilder.DropTable( + name: "Sessions"); + + migrationBuilder.DropTable( + name: "Tracks"); + } + } +} diff --git a/code/session-7/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs b/code/session-7/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs index a66dfe1..6cc21a5 100644 --- a/code/session-7/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/code/session-7/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs @@ -4,8 +4,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -namespace GraphQL.Migrations +#nullable disable + +namespace ConferencePlanner.GraphQL.Migrations { [DbContext(typeof(ApplicationDbContext))] partial class ApplicationDbContextModelSnapshot : ModelSnapshot @@ -14,36 +17,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("EmailAddress") .HasMaxLength(256) - .HasColumnType("TEXT"); + .HasColumnType("character varying(256)"); b.Property("FirstName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("LastName") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("UserName") + b.Property("Username") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); - b.HasIndex("UserName") + b.HasIndex("Username") .IsUnique(); b.ToTable("Attendees"); @@ -53,25 +61,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Abstract") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("EndTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("StartTime") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.Property("Title") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.Property("TrackId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("Id"); @@ -83,10 +93,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("AttendeeId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "AttendeeId"); @@ -98,10 +108,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => { b.Property("SessionId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("SpeakerId") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.HasKey("SessionId", "SpeakerId"); @@ -114,20 +124,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Bio") .HasMaxLength(4000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(4000)"); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); - b.Property("WebSite") + b.Property("Website") .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("character varying(1000)"); b.HasKey("Id"); @@ -138,12 +150,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("Name") .IsRequired() .HasMaxLength(200) - .HasColumnType("TEXT"); + .HasColumnType("character varying(200)"); b.HasKey("Id"); diff --git a/code/session-7/GraphQL/Program.cs b/code/session-7/GraphQL/Program.cs index c4914c6..6598afb 100644 --- a/code/session-7/GraphQL/Program.cs +++ b/code/session-7/GraphQL/Program.cs @@ -1,26 +1,25 @@ -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 ConferencePlanner.GraphQL -{ - 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(); - }); - } -} +using ConferencePlanner.GraphQL.Data; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddDbContext( + options => options.UseNpgsql("Host=127.0.0.1;Username=graphql_workshop;Password=secret")) + .AddGraphQLServer() + .AddGlobalObjectIdentification() + .AddMutationConventions() + .AddDbContextCursorPagingProvider() + .AddPagingArguments() + .AddFiltering() + .AddSorting() + .AddRedisSubscriptions(_ => ConnectionMultiplexer.Connect("127.0.0.1:6379")) + .AddGraphQLTypes(); + +var app = builder.Build(); + +app.UseWebSockets(); +app.MapGraphQL(); + +await app.RunWithGraphQLCommandsAsync(args); diff --git a/code/session-7/GraphQL/Properties/launchSettings.json b/code/session-7/GraphQL/Properties/launchSettings.json new file mode 100644 index 0000000..c0d9484 --- /dev/null +++ b/code/session-7/GraphQL/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7000;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/code/session-7/GraphQL/Sessions/AddSessionInput.cs b/code/session-7/GraphQL/Sessions/AddSessionInput.cs index db5995f..3474bf3 100644 --- a/code/session-7/GraphQL/Sessions/AddSessionInput.cs +++ b/code/session-7/GraphQL/Sessions/AddSessionInput.cs @@ -1,12 +1,8 @@ -using System.Collections.Generic; using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; -namespace ConferencePlanner.GraphQL.Sessions -{ - public record AddSessionInput( - string Title, - string? Abstract, - [ID(nameof(Speaker))] - IReadOnlyList SpeakerIds); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed record AddSessionInput( + string Title, + string? Abstract, + [property: ID] IReadOnlyList SpeakerIds); diff --git a/code/session-7/GraphQL/Sessions/AddSessionPayload.cs b/code/session-7/GraphQL/Sessions/AddSessionPayload.cs deleted file mode 100644 index 82775f8..0000000 --- a/code/session-7/GraphQL/Sessions/AddSessionPayload.cs +++ /dev/null @@ -1,20 +0,0 @@ -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class AddSessionPayload : Payload - { - public AddSessionPayload(Session session) - { - Session = session; - } - - public AddSessionPayload(UserError error) - : base(new[] { error }) - { - } - - public Session? Session { get; init; } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Sessions/ScheduleSessionInput.cs b/code/session-7/GraphQL/Sessions/ScheduleSessionInput.cs index fc43463..9c4fd11 100644 --- a/code/session-7/GraphQL/Sessions/ScheduleSessionInput.cs +++ b/code/session-7/GraphQL/Sessions/ScheduleSessionInput.cs @@ -1,14 +1,9 @@ -using System; using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; -namespace ConferencePlanner.GraphQL.Sessions -{ - public record ScheduleSessionInput( - [ID(nameof(Session))] - int SessionId, - [ID(nameof(Track))] - int TrackId, - DateTimeOffset StartTime, - DateTimeOffset EndTime); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed record ScheduleSessionInput( + [property: ID] int SessionId, + [property: ID] int TrackId, + DateTimeOffset StartTime, + DateTimeOffset EndTime); diff --git a/code/session-7/GraphQL/Sessions/ScheduleSessionPayload.cs b/code/session-7/GraphQL/Sessions/ScheduleSessionPayload.cs deleted file mode 100644 index ce79df5..0000000 --- a/code/session-7/GraphQL/Sessions/ScheduleSessionPayload.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class ScheduleSessionPayload : SessionPayloadBase - { - public ScheduleSessionPayload(Session session) - : base(session) - { - } - - public ScheduleSessionPayload(UserError error) - : base(new[] { error }) - { - } - - public async Task GetTrackAsync( - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) - { - if (Session is null) - { - return null; - } - - return await trackById.LoadAsync(Session.Id, cancellationToken); - } - - [UseApplicationDbContext] - public async Task?> GetSpeakersAsync( - [ScopedService] ApplicationDbContext dbContext, - SpeakerByIdDataLoader speakerById, - CancellationToken cancellationToken) - { - if (Session is null) - { - return null; - } - - int[] speakerIds = await dbContext.Sessions - .Where(s => s.Id == Session.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SpeakerId)) - .ToArrayAsync(); - - return await speakerById.LoadAsync(speakerIds, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Sessions/SessionDataLoaders.cs b/code/session-7/GraphQL/Sessions/SessionDataLoaders.cs new file mode 100644 index 0000000..738ce5e --- /dev/null +++ b/code/session-7/GraphQL/Sessions/SessionDataLoaders.cs @@ -0,0 +1,50 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Sessions; + +public static class SessionDataLoaders +{ + [DataLoader] + public static async Task> SessionByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => ids.Contains(s.Id)) + .Select(s => s.Id, selector) + .ToDictionaryAsync(s => s.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SpeakersBySessionIdAsync( + IReadOnlyList sessionIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => sessionIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionSpeakers.Select(ss => ss.Speaker), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } + + [DataLoader] + public static async Task> AttendeesBySessionIdAsync( + IReadOnlyList sessionIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => sessionIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionAttendees.Select(sa => sa.Attendee), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-7/GraphQL/Sessions/SessionExceptions.cs b/code/session-7/GraphQL/Sessions/SessionExceptions.cs new file mode 100644 index 0000000..fea5d77 --- /dev/null +++ b/code/session-7/GraphQL/Sessions/SessionExceptions.cs @@ -0,0 +1,9 @@ +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed class EndTimeInvalidException() : Exception("EndTime must be after StartTime."); + +public sealed class NoSpeakerException() : Exception("No speaker assigned."); + +public sealed class SessionNotFoundException() : Exception("Session not found."); + +public sealed class TitleEmptyException() : Exception("The title cannot be empty."); diff --git a/code/session-7/GraphQL/Sessions/SessionFilterInputType.cs b/code/session-7/GraphQL/Sessions/SessionFilterInputType.cs new file mode 100644 index 0000000..b6096b6 --- /dev/null +++ b/code/session-7/GraphQL/Sessions/SessionFilterInputType.cs @@ -0,0 +1,17 @@ +using ConferencePlanner.GraphQL.Data; +using HotChocolate.Data.Filters; + +namespace ConferencePlanner.GraphQL.Sessions; + +public sealed class SessionFilterInputType : FilterInputType +{ + protected override void Configure(IFilterInputTypeDescriptor descriptor) + { + descriptor.BindFieldsExplicitly(); + + descriptor.Field(s => s.Title); + descriptor.Field(s => s.Abstract); + descriptor.Field(s => s.StartTime); + descriptor.Field(s => s.EndTime); + } +} diff --git a/code/session-7/GraphQL/Sessions/SessionMutations.cs b/code/session-7/GraphQL/Sessions/SessionMutations.cs index ac6148f..5986337 100644 --- a/code/session-7/GraphQL/Sessions/SessionMutations.cs +++ b/code/session-7/GraphQL/Sessions/SessionMutations.cs @@ -1,86 +1,80 @@ -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Common; using ConferencePlanner.GraphQL.Data; -using HotChocolate; using HotChocolate.Subscriptions; -using HotChocolate.Types; -namespace ConferencePlanner.GraphQL.Sessions +namespace ConferencePlanner.GraphQL.Sessions; + +[MutationType] +public static class SessionMutations { - [ExtendObjectType(Name = "Mutation")] - public class SessionMutations + [Error] + [Error] + public static async Task AddSessionAsync( + AddSessionInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - [UseApplicationDbContext] - public async Task AddSessionAsync( - AddSessionInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) + if (string.IsNullOrEmpty(input.Title)) { - if (string.IsNullOrEmpty(input.Title)) - { - return new AddSessionPayload( - new UserError("The title cannot be empty.", "TITLE_EMPTY")); - } + throw new TitleEmptyException(); + } - if (input.SpeakerIds.Count == 0) - { - return new AddSessionPayload( - new UserError("No speaker assigned.", "NO_SPEAKER")); - } + if (input.SpeakerIds.Count == 0) + { + throw new NoSpeakerException(); + } - var session = new Session - { - Title = input.Title, - Abstract = input.Abstract, - }; + var session = new Session + { + Title = input.Title, + Abstract = input.Abstract + }; - foreach (int speakerId in input.SpeakerIds) + foreach (var speakerId in input.SpeakerIds) + { + session.SessionSpeakers.Add(new SessionSpeaker { - session.SessionSpeakers.Add(new SessionSpeaker - { - SpeakerId = speakerId - }); - } + SpeakerId = speakerId + }); + } - context.Sessions.Add(session); - await context.SaveChangesAsync(cancellationToken); + dbContext.Sessions.Add(session); - return new AddSessionPayload(session); - } + await dbContext.SaveChangesAsync(cancellationToken); + + return session; + } - [UseApplicationDbContext] - public async Task ScheduleSessionAsync( - ScheduleSessionInput input, - [ScopedService] ApplicationDbContext context, - [Service]ITopicEventSender eventSender) + [Error] + [Error] + public static async Task ScheduleSessionAsync( + ScheduleSessionInput input, + ApplicationDbContext dbContext, + ITopicEventSender eventSender, + CancellationToken cancellationToken) + { + if (input.EndTime < input.StartTime) { - if (input.EndTime < input.StartTime) - { - return new ScheduleSessionPayload( - new UserError("endTime has to be larger than startTime.", "END_TIME_INVALID")); - } + throw new EndTimeInvalidException(); + } - Session session = await context.Sessions.FindAsync(input.SessionId); - int? initialTrackId = session.TrackId; + var session = await dbContext.Sessions.FindAsync([input.SessionId], cancellationToken); - if (session is null) - { - return new ScheduleSessionPayload( - new UserError("Session not found.", "SESSION_NOT_FOUND")); - } + if (session is null) + { + throw new SessionNotFoundException(); + } - session.TrackId = input.TrackId; - session.StartTime = input.StartTime; - session.EndTime = input.EndTime; + session.TrackId = input.TrackId; + session.StartTime = input.StartTime; + session.EndTime = input.EndTime; - await context.SaveChangesAsync(); + await dbContext.SaveChangesAsync(cancellationToken); - await eventSender.SendAsync( - nameof(SessionSubscriptions.OnSessionScheduledAsync), - session.Id); + await eventSender.SendAsync( + nameof(SessionSubscriptions.OnSessionScheduledAsync), + session.Id, + cancellationToken); - return new ScheduleSessionPayload(session); - } + return session; } -} \ No newline at end of file +} diff --git a/code/session-7/GraphQL/Sessions/SessionPayloadBase.cs b/code/session-7/GraphQL/Sessions/SessionPayloadBase.cs deleted file mode 100644 index 888ad50..0000000 --- a/code/session-7/GraphQL/Sessions/SessionPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class SessionPayloadBase : Payload - { - protected SessionPayloadBase(Session session) - { - Session = session; - } - - protected SessionPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Session? Session { get; } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Sessions/SessionQueries.cs b/code/session-7/GraphQL/Sessions/SessionQueries.cs index 1cacdfd..7c1294c 100644 --- a/code/session-7/GraphQL/Sessions/SessionQueries.cs +++ b/code/session-7/GraphQL/Sessions/SessionQueries.cs @@ -1,40 +1,37 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; -using System.Linq; -using ConferencePlanner.GraphQL.Types; -using HotChocolate.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; -namespace ConferencePlanner.GraphQL.Sessions +namespace ConferencePlanner.GraphQL.Sessions; + +[QueryType] +public static class SessionQueries { - [ExtendObjectType(Name = "Query")] - public class SessionQueries + [UsePaging] + [UseFiltering] + [UseSorting] + public static IQueryable GetSessions(ApplicationDbContext dbContext) { - [UseApplicationDbContext] - [UsePaging(typeof(NonNullType))] - // TODO: [UseFiltering(typeof(SessionFilterInputType))] - [UseFiltering] - [UseSorting] - public IQueryable GetSessions( - [ScopedService] ApplicationDbContext context) => - context.Sessions; + return dbContext.Sessions.AsNoTracking().OrderBy(s => s.Title).ThenBy(s => s.Id); + } - public Task GetSessionByIdAsync( - [ID(nameof(Session))] int id, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - sessionById.LoadAsync(id, cancellationToken); + [NodeResolver] + public static async Task GetSessionByIdAsync( + int id, + ISessionByIdDataLoader sessionById, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionById.Select(selection).LoadAsync(id, cancellationToken); + } - public async Task> GetSessionsByIdAsync( - [ID(nameof(Session))] int[] ids, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - await sessionById.LoadAsync(ids, cancellationToken); + public static async Task> GetSessionsByIdAsync( + [ID] int[] ids, + ISessionByIdDataLoader sessionById, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionById.Select(selection).LoadRequiredAsync(ids, cancellationToken); } -} \ No newline at end of file +} diff --git a/code/session-7/GraphQL/Sessions/SessionSubscriptions.cs b/code/session-7/GraphQL/Sessions/SessionSubscriptions.cs index fef037a..5e54e24 100644 --- a/code/session-7/GraphQL/Sessions/SessionSubscriptions.cs +++ b/code/session-7/GraphQL/Sessions/SessionSubscriptions.cs @@ -1,21 +1,17 @@ -using System.Threading; -using System.Threading.Tasks; using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -namespace ConferencePlanner.GraphQL.Sessions +namespace ConferencePlanner.GraphQL.Sessions; + +[SubscriptionType] +public static class SessionSubscriptions { - [ExtendObjectType(Name = "Subscription")] - public class SessionSubscriptions + [Subscribe] + [Topic] + public static async Task OnSessionScheduledAsync( + [EventMessage] int sessionId, + ISessionByIdDataLoader sessionById, + CancellationToken cancellationToken) { - [Subscribe] - [Topic] - public Task OnSessionScheduledAsync( - [EventMessage] int sessionId, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - sessionById.LoadAsync(sessionId, cancellationToken); + return await sessionById.LoadRequiredAsync(sessionId, cancellationToken); } -} \ No newline at end of file +} diff --git a/code/session-7/GraphQL/Sessions/SessionType.cs b/code/session-7/GraphQL/Sessions/SessionType.cs new file mode 100644 index 0000000..abda805 --- /dev/null +++ b/code/session-7/GraphQL/Sessions/SessionType.cs @@ -0,0 +1,60 @@ +using ConferencePlanner.GraphQL.Data; +using ConferencePlanner.GraphQL.Tracks; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Sessions; + +[ObjectType] +public static partial class SessionType +{ + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field(s => s.TrackId) + .ID(); + } + + public static TimeSpan Duration([Parent("StartTime EndTime")] Session session) + => session.Duration; + + [BindMember(nameof(Session.SessionSpeakers))] + public static async Task> GetSpeakersAsync( + [Parent] Session session, + ISpeakersBySessionIdDataLoader speakersBySessionId, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakersBySessionId + .Select(selection) + .LoadRequiredAsync(session.Id, cancellationToken); + } + + [BindMember(nameof(Session.SessionAttendees))] + public static async Task> GetAttendeesAsync( + [Parent(nameof(Session.Id))] Session session, + IAttendeesBySessionIdDataLoader attendeesBySessionId, + ISelection selection, + CancellationToken cancellationToken) + { + return await attendeesBySessionId + .Select(selection) + .LoadRequiredAsync(session.Id, cancellationToken); + } + + public static async Task GetTrackAsync( + [Parent(nameof(Session.TrackId))] Session session, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + if (session.TrackId is null) + { + return null; + } + + return await trackById + .Select(selection) + .LoadAsync(session.TrackId.Value, cancellationToken); + } +} diff --git a/code/session-7/GraphQL/Speakers/AddSpeakerInput.cs b/code/session-7/GraphQL/Speakers/AddSpeakerInput.cs index a81f45f..bdc584a 100644 --- a/code/session-7/GraphQL/Speakers/AddSpeakerInput.cs +++ b/code/session-7/GraphQL/Speakers/AddSpeakerInput.cs @@ -1,7 +1,6 @@ -namespace ConferencePlanner.GraphQL.Speakers -{ - public record AddSpeakerInput( - string Name, - string? Bio, - string? WebSite); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Speakers; + +public sealed record AddSpeakerInput( + string Name, + string? Bio, + string? Website); diff --git a/code/session-7/GraphQL/Speakers/AddSpeakerPayload.cs b/code/session-7/GraphQL/Speakers/AddSpeakerPayload.cs deleted file mode 100644 index aaf0ab0..0000000 --- a/code/session-7/GraphQL/Speakers/AddSpeakerPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Speakers -{ - public class AddSpeakerPayload : SpeakerPayloadBase - { - public AddSpeakerPayload(Speaker speaker) - : base(speaker) - { - } - - public AddSpeakerPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Speakers/SpeakerDataLoaders.cs b/code/session-7/GraphQL/Speakers/SpeakerDataLoaders.cs new file mode 100644 index 0000000..c2c748e --- /dev/null +++ b/code/session-7/GraphQL/Speakers/SpeakerDataLoaders.cs @@ -0,0 +1,36 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Speakers; + +public static class SpeakerDataLoaders +{ + [DataLoader] + public static async Task> SpeakerByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Speakers + .AsNoTracking() + .Where(s => ids.Contains(s.Id)) + .Select(s => s.Id, selector) + .ToDictionaryAsync(s => s.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SessionsBySpeakerIdAsync( + IReadOnlyList speakerIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Speakers + .AsNoTracking() + .Where(s => speakerIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionSpeakers.Select(ss => ss.Session), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } +} diff --git a/code/session-7/GraphQL/Speakers/SpeakerMutations.cs b/code/session-7/GraphQL/Speakers/SpeakerMutations.cs index 55fc6f5..0a8ad7a 100644 --- a/code/session-7/GraphQL/Speakers/SpeakerMutations.cs +++ b/code/session-7/GraphQL/Speakers/SpeakerMutations.cs @@ -1,29 +1,26 @@ -using System.Threading.Tasks; using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; -namespace ConferencePlanner.GraphQL.Speakers +namespace ConferencePlanner.GraphQL.Speakers; + +[MutationType] +public static class SpeakerMutations { - [ExtendObjectType(Name = "Mutation")] - public class SpeakerMutations + public static async Task AddSpeakerAsync( + AddSpeakerInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - [UseApplicationDbContext] - public async Task AddSpeakerAsync( - AddSpeakerInput input, - [ScopedService] ApplicationDbContext context) + var speaker = new Speaker { - var speaker = new Speaker - { - Name = input.Name, - Bio = input.Bio, - WebSite = input.WebSite - }; + Name = input.Name, + Bio = input.Bio, + Website = input.Website + }; + + dbContext.Speakers.Add(speaker); - context.Speakers.Add(speaker); - await context.SaveChangesAsync(); + await dbContext.SaveChangesAsync(cancellationToken); - return new AddSpeakerPayload(speaker); - } + return speaker; } -} \ No newline at end of file +} diff --git a/code/session-7/GraphQL/Speakers/SpeakerPayloadBase.cs b/code/session-7/GraphQL/Speakers/SpeakerPayloadBase.cs deleted file mode 100644 index a2077d7..0000000 --- a/code/session-7/GraphQL/Speakers/SpeakerPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Speakers -{ - public class SpeakerPayloadBase : Payload - { - protected SpeakerPayloadBase(Speaker speaker) - { - Speaker = speaker; - } - - protected SpeakerPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Speaker? Speaker { get; init; } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Speakers/SpeakerQueries.cs b/code/session-7/GraphQL/Speakers/SpeakerQueries.cs index c9b57b8..8283587 100644 --- a/code/session-7/GraphQL/Speakers/SpeakerQueries.cs +++ b/code/session-7/GraphQL/Speakers/SpeakerQueries.cs @@ -1,35 +1,35 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; -using System.Linq; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; -namespace ConferencePlanner.GraphQL.Speakers +namespace ConferencePlanner.GraphQL.Speakers; + +[QueryType] +public static class SpeakerQueries { - [ExtendObjectType(Name = "Query")] - public class SpeakerQueries + [UsePaging] + public static IQueryable GetSpeakers(ApplicationDbContext dbContext) { - [UseApplicationDbContext] - [UsePaging] - public IQueryable GetSpeakers( - [ScopedService] ApplicationDbContext context) => - context.Speakers.OrderBy(t => t.Name); + return dbContext.Speakers.AsNoTracking().OrderBy(s => s.Name).ThenBy(s => s.Id); + } - public Task GetSpeakerByIdAsync( - [ID(nameof(Speaker))]int id, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - dataLoader.LoadAsync(id, cancellationToken); + [NodeResolver] + public static async Task GetSpeakerByIdAsync( + int id, + ISpeakerByIdDataLoader speakerById, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakerById.Select(selection).LoadAsync(id, cancellationToken); + } - public async Task> GetSpeakersByIdAsync( - [ID(nameof(Speaker))]int[] ids, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - await dataLoader.LoadAsync(ids, cancellationToken); + public static async Task> GetSpeakersByIdAsync( + [ID] int[] ids, + ISpeakerByIdDataLoader speakerById, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakerById.Select(selection).LoadRequiredAsync(ids, cancellationToken); } -} \ No newline at end of file +} diff --git a/code/session-7/GraphQL/Speakers/SpeakerType.cs b/code/session-7/GraphQL/Speakers/SpeakerType.cs new file mode 100644 index 0000000..54555fd --- /dev/null +++ b/code/session-7/GraphQL/Speakers/SpeakerType.cs @@ -0,0 +1,21 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; + +namespace ConferencePlanner.GraphQL.Speakers; + +[ObjectType] +public static partial class SpeakerType +{ + [BindMember(nameof(Speaker.SessionSpeakers))] + public static async Task> GetSessionsAsync( + [Parent] Speaker speaker, + ISessionsBySpeakerIdDataLoader sessionsBySpeakerId, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsBySpeakerId + .Select(selection) + .LoadRequiredAsync(speaker.Id, cancellationToken); + } +} diff --git a/code/session-7/GraphQL/Startup.cs b/code/session-7/GraphQL/Startup.cs deleted file mode 100644 index efcc20a..0000000 --- a/code/session-7/GraphQL/Startup.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL; -using ConferencePlanner.GraphQL.Attendees; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using ConferencePlanner.GraphQL.Sessions; -using ConferencePlanner.GraphQL.Speakers; -using ConferencePlanner.GraphQL.Tracks; -using ConferencePlanner.GraphQL.Types; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace ConferencePlanner.GraphQL -{ - 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.AddPooledDbContextFactory( - options => options.UseSqlite("Data Source=conferences.db")); - - services - .AddGraphQLServer() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddSubscriptionType(d => d.Name("Subscription")) - .AddTypeExtension() - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport() - .AddFiltering() - .AddSorting() - .AddInMemorySubscriptions() - .AddDataLoader() - .AddDataLoader(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseWebSockets(); - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGraphQL(); - }); - } - } -} diff --git a/code/session-7/GraphQL/Tracks/AddTrackInput.cs b/code/session-7/GraphQL/Tracks/AddTrackInput.cs index 5c83b34..1aaf313 100644 --- a/code/session-7/GraphQL/Tracks/AddTrackInput.cs +++ b/code/session-7/GraphQL/Tracks/AddTrackInput.cs @@ -1,4 +1,3 @@ -namespace ConferencePlanner.GraphQL.Tracks -{ - public record AddTrackInput(string Name); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Tracks; + +public sealed record AddTrackInput(string Name); diff --git a/code/session-7/GraphQL/Tracks/AddTrackPayload.cs b/code/session-7/GraphQL/Tracks/AddTrackPayload.cs deleted file mode 100644 index 8f35b13..0000000 --- a/code/session-7/GraphQL/Tracks/AddTrackPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class AddTrackPayload : TrackPayloadBase - { - public AddTrackPayload(Track track) - : base(track) - { - } - - public AddTrackPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Tracks/RenameTrackInput.cs b/code/session-7/GraphQL/Tracks/RenameTrackInput.cs index 516c6a0..d11ad39 100644 --- a/code/session-7/GraphQL/Tracks/RenameTrackInput.cs +++ b/code/session-7/GraphQL/Tracks/RenameTrackInput.cs @@ -1,7 +1,5 @@ using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; -namespace ConferencePlanner.GraphQL.Tracks -{ - public record RenameTrackInput([ID(nameof(Track))] int Id, string Name); -} \ No newline at end of file +namespace ConferencePlanner.GraphQL.Tracks; + +public sealed record RenameTrackInput([property: ID] int Id, string Name); diff --git a/code/session-7/GraphQL/Tracks/RenameTrackPayload.cs b/code/session-7/GraphQL/Tracks/RenameTrackPayload.cs deleted file mode 100644 index ca4c8a1..0000000 --- a/code/session-7/GraphQL/Tracks/RenameTrackPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class RenameTrackPayload : TrackPayloadBase - { - public RenameTrackPayload(Track track) - : base(track) - { - } - - public RenameTrackPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Tracks/TrackDataLoaders.cs b/code/session-7/GraphQL/Tracks/TrackDataLoaders.cs new file mode 100644 index 0000000..f592b8a --- /dev/null +++ b/code/session-7/GraphQL/Tracks/TrackDataLoaders.cs @@ -0,0 +1,38 @@ +using ConferencePlanner.GraphQL.Data; +using GreenDonut.Data; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Tracks; + +public static class TrackDataLoaders +{ + [DataLoader] + public static async Task> TrackByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Tracks + .AsNoTracking() + .Where(t => ids.Contains(t.Id)) + .Select(t => t.Id, selector) + .ToDictionaryAsync(t => t.Id, cancellationToken); + } + + [DataLoader] + public static async Task>> SessionsByTrackIdAsync( + IReadOnlyList trackIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + PagingArguments pagingArguments, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => s.TrackId != null && trackIds.Contains((int)s.TrackId)) + .OrderBy(s => s.Id) + .Select(s => s.TrackId, selector) + .ToBatchPageAsync(s => (int)s.TrackId!, pagingArguments, cancellationToken); + } +} diff --git a/code/session-7/GraphQL/Tracks/TrackExceptions.cs b/code/session-7/GraphQL/Tracks/TrackExceptions.cs new file mode 100644 index 0000000..8df488d --- /dev/null +++ b/code/session-7/GraphQL/Tracks/TrackExceptions.cs @@ -0,0 +1,3 @@ +namespace ConferencePlanner.GraphQL.Tracks; + +public sealed class TrackNotFoundException() : Exception("Track not found."); diff --git a/code/session-7/GraphQL/Tracks/TrackMutations.cs b/code/session-7/GraphQL/Tracks/TrackMutations.cs index 88727b9..a91671c 100644 --- a/code/session-7/GraphQL/Tracks/TrackMutations.cs +++ b/code/session-7/GraphQL/Tracks/TrackMutations.cs @@ -1,40 +1,41 @@ -using System.Threading; -using System.Threading.Tasks; using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; -namespace ConferencePlanner.GraphQL.Tracks +namespace ConferencePlanner.GraphQL.Tracks; + +[MutationType] +public static class TrackMutations { - [ExtendObjectType(Name = "Mutation")] - public class TrackMutations + public static async Task AddTrackAsync( + AddTrackInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - [UseApplicationDbContext] - public async Task AddTrackAsync( - AddTrackInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - var track = new Track { Name = input.Name }; - context.Tracks.Add(track); + var track = new Track { Name = input.Name }; - await context.SaveChangesAsync(cancellationToken); + dbContext.Tracks.Add(track); - return new AddTrackPayload(track); - } + await dbContext.SaveChangesAsync(cancellationToken); - [UseApplicationDbContext] - public async Task RenameTrackAsync( - RenameTrackInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - Track track = await context.Tracks.FindAsync(input.Id); - track.Name = input.Name; + return track; + } - await context.SaveChangesAsync(cancellationToken); + [Error] + public static async Task RenameTrackAsync( + RenameTrackInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var track = await dbContext.Tracks.FindAsync([input.Id], cancellationToken); - return new RenameTrackPayload(track); + if (track is null) + { + throw new TrackNotFoundException(); } + + track.Name = input.Name; + + await dbContext.SaveChangesAsync(cancellationToken); + + return track; } -} \ No newline at end of file +} diff --git a/code/session-7/GraphQL/Tracks/TrackPayloadBase.cs b/code/session-7/GraphQL/Tracks/TrackPayloadBase.cs deleted file mode 100644 index de11da7..0000000 --- a/code/session-7/GraphQL/Tracks/TrackPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class TrackPayloadBase : Payload - { - public TrackPayloadBase(Track track) - { - Track = track; - } - - public TrackPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Track? Track { get; } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Tracks/TrackQueries.cs b/code/session-7/GraphQL/Tracks/TrackQueries.cs index 7ced249..948efac 100644 --- a/code/session-7/GraphQL/Tracks/TrackQueries.cs +++ b/code/session-7/GraphQL/Tracks/TrackQueries.cs @@ -1,49 +1,35 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using Microsoft.EntityFrameworkCore; + +namespace ConferencePlanner.GraphQL.Tracks; -namespace ConferencePlanner.GraphQL.Tracks +[QueryType] +public static class TrackQueries { - [ExtendObjectType(Name = "Query")] - public class TrackQueries + [UsePaging] + public static IQueryable GetTracks(ApplicationDbContext dbContext) { - [UseApplicationDbContext] - [UsePaging] - public IQueryable GetTracks( - [ScopedService] ApplicationDbContext context) => - context.Tracks.OrderBy(t => t.Name); - - [UseApplicationDbContext] - public Task GetTrackByNameAsync( - string name, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - context.Tracks.FirstAsync(t => t.Name == name); - - [UseApplicationDbContext] - public async Task> GetTrackByNamesAsync( - string[] names, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - await context.Tracks.Where(t => names.Contains(t.Name)).ToListAsync(); + return dbContext.Tracks.AsNoTracking().OrderBy(t => t.Name).ThenBy(t => t.Id); + } - public Task GetTrackByIdAsync( - [ID(nameof(Track))] int id, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) => - trackById.LoadAsync(id, cancellationToken); + [NodeResolver] + public static async Task GetTrackByIdAsync( + int id, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + return await trackById.Select(selection).LoadAsync(id, cancellationToken); + } - public async Task> GetTracksByIdAsync( - [ID(nameof(Track))] int[] ids, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) => - await trackById.LoadAsync(ids, cancellationToken); + public static async Task> GetTracksByIdAsync( + [ID] int[] ids, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + return await trackById.Select(selection).LoadRequiredAsync(ids, cancellationToken); } -} \ No newline at end of file +} diff --git a/code/session-7/GraphQL/Tracks/TrackType.cs b/code/session-7/GraphQL/Tracks/TrackType.cs new file mode 100644 index 0000000..2bc3559 --- /dev/null +++ b/code/session-7/GraphQL/Tracks/TrackType.cs @@ -0,0 +1,34 @@ +using ConferencePlanner.GraphQL.Data; +using ConferencePlanner.GraphQL.Extensions; +using GreenDonut.Data; +using HotChocolate.Execution.Processing; +using HotChocolate.Types.Pagination; + +namespace ConferencePlanner.GraphQL.Tracks; + +[ObjectType] +public static partial class TrackType +{ + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field(t => t.Name) + .ParentRequires(nameof(Track.Name)) + .UseUpperCase(); + } + + [UsePaging] + public static async Task> GetSessionsAsync( + [Parent] Track track, + ISessionsByTrackIdDataLoader sessionsByTrackId, + PagingArguments pagingArguments, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsByTrackId + .With(pagingArguments) + .Select(selection) + .LoadAsync(track.Id, cancellationToken) + .ToConnectionAsync(); + } +} diff --git a/code/session-7/GraphQL/Types/AttendeeType.cs b/code/session-7/GraphQL/Types/AttendeeType.cs deleted file mode 100644 index 62274c7..0000000 --- a/code/session-7/GraphQL/Types/AttendeeType.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Types -{ - public class AttendeeType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionsAttendees) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("sessions"); - } - - private class AttendeeResolvers - { - public async Task> GetSessionsAsync( - Attendee attendee, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Attendees - .Where(a => a.Id == attendee.Id) - .Include(a => a.SessionsAttendees) - .SelectMany(a => a.SessionsAttendees.Select(t => t.SessionId)) - .ToArrayAsync(); - - return await sessionById.LoadAsync(speakerIds, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Types/SessionFilterInputType.cs b/code/session-7/GraphQL/Types/SessionFilterInputType.cs deleted file mode 100644 index 9a514e8..0000000 --- a/code/session-7/GraphQL/Types/SessionFilterInputType.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Data.Filters; - -namespace ConferencePlanner.GraphQL.Types -{ - public class SessionFilterInputType : FilterInputType - { - protected override void Configure(IFilterInputTypeDescriptor descriptor) - { - descriptor.Ignore(t => t.Id); - descriptor.Ignore(t => t.TrackId); - } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Types/SessionType.cs b/code/session-7/GraphQL/Types/SessionType.cs deleted file mode 100644 index 8a558f7..0000000 --- a/code/session-7/GraphQL/Types/SessionType.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Types -{ - public class SessionType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionSpeakers) - .ResolveWith(t => t.GetSpeakersAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("speakers"); - - descriptor - .Field(t => t.SessionAttendees) - .ResolveWith(t => t.GetAttendeesAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("attendees"); - - descriptor - .Field(t => t.Track) - .ResolveWith(t => t.GetTrackAsync(default!, default!, default)); - - descriptor - .Field(t => t.TrackId) - .ID(nameof(Track)); - } - - private class SessionResolvers - { - public async Task> GetSpeakersAsync( - Session session, - [ScopedService] ApplicationDbContext dbContext, - SpeakerByIdDataLoader speakerById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Sessions - .Where(s => s.Id == session.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SpeakerId)) - .ToArrayAsync(); - - return await speakerById.LoadAsync(speakerIds, cancellationToken); - } - - public async Task> GetAttendeesAsync( - Session session, - [ScopedService] ApplicationDbContext dbContext, - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) - { - int[] attendeeIds = await dbContext.Sessions - .Where(s => s.Id == session.Id) - .Include(session => session.SessionAttendees) - .SelectMany(session => session.SessionAttendees.Select(t => t.AttendeeId)) - .ToArrayAsync(); - - return await attendeeById.LoadAsync(attendeeIds, cancellationToken); - } - - public async Task GetTrackAsync( - Session session, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) - { - if (session.TrackId is null) - { - return null; - } - - return await trackById.LoadAsync(session.TrackId.Value, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Types/SpeakerType.cs b/code/session-7/GraphQL/Types/SpeakerType.cs deleted file mode 100644 index 89837f0..0000000 --- a/code/session-7/GraphQL/Types/SpeakerType.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Types -{ - public class SpeakerType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionSpeakers) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("sessions"); - } - - private class SpeakerResolvers - { - public async Task> GetSessionsAsync( - Speaker speaker, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Speakers - .Where(s => s.Id == speaker.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SessionId)) - .ToArrayAsync(); - - return await sessionById.LoadAsync(speakerIds, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/Types/TrackType.cs b/code/session-7/GraphQL/Types/TrackType.cs deleted file mode 100644 index dfa4faf..0000000 --- a/code/session-7/GraphQL/Types/TrackType.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Types -{ - public class TrackType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => - ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.Sessions) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .UsePaging>() - .Name("sessions"); - - descriptor - .Field(t => t.Name) - .UseUpperCase(); - } - - private class TrackResolvers - { - public async Task> GetSessionsAsync( - Track track, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] sessionIds = await dbContext.Sessions - .Where(s => s.Id == track.Id) - .Select(s => s.Id) - .ToArrayAsync(); - - return await sessionById.LoadAsync(sessionIds, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-7/GraphQL/appsettings.Development.json b/code/session-7/GraphQL/appsettings.Development.json index dba68eb..0c208ae 100644 --- a/code/session-7/GraphQL/appsettings.Development.json +++ b/code/session-7/GraphQL/appsettings.Development.json @@ -1,9 +1,8 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Information", + "Microsoft.AspNetCore": "Warning" } } } diff --git a/code/session-7/GraphQL/appsettings.json b/code/session-7/GraphQL/appsettings.json index 81ff877..10f68b8 100644 --- a/code/session-7/GraphQL/appsettings.json +++ b/code/session-7/GraphQL/appsettings.json @@ -1,10 +1,9 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/code/session-7/docker-compose.yml b/code/session-7/docker-compose.yml new file mode 100644 index 0000000..5785c83 --- /dev/null +++ b/code/session-7/docker-compose.yml @@ -0,0 +1,33 @@ +name: graphql-workshop + +services: + graphql-workshop-postgres: + container_name: graphql-workshop-postgres + image: postgres:17.2 + environment: + POSTGRES_USER: graphql_workshop + POSTGRES_PASSWORD: secret + POSTGRES_DB: graphql_workshop + networks: [graphql-workshop] + ports: ["5432:5432"] + volumes: + - type: volume + source: postgres-data + target: /var/lib/postgresql/data + graphql-workshop-redis: + container_name: graphql-workshop-redis + image: redis:7.4 + networks: [graphql-workshop] + ports: ["6379:6379"] + volumes: + - type: volume + source: redis-data + target: /data + +networks: + graphql-workshop: + name: graphql-workshop + +volumes: + postgres-data: + redis-data: diff --git a/code/session-8/.config/dotnet-tools.json b/code/session-8/.config/dotnet-tools.json deleted file mode 100644 index c735fef..0000000 --- a/code/session-8/.config/dotnet-tools.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "dotnet-ef": { - "version": "5.0.0", - "commands": [ - "dotnet-ef" - ] - } - } -} \ No newline at end of file diff --git a/code/session-8/.vscode/launch.json b/code/session-8/.vscode/launch.json deleted file mode 100644 index e90375e..0000000 --- a/code/session-8/.vscode/launch.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - // 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}/GraphQL/bin/Debug/net5.0/GraphQL.dll", - "args": [], - "cwd": "${workspaceFolder}/GraphQL", - "stopAtEntry": false, - "serverReadyAction": { - "action": "openExternally", - "pattern": "\\bNow 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/code/session-8/.vscode/tasks.json b/code/session-8/.vscode/tasks.json deleted file mode 100644 index 31c32bd..0000000 --- a/code/session-8/.vscode/tasks.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - // 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/code/session-8/ConferencePlanner.sln b/code/session-8/ConferencePlanner.sln deleted file mode 100644 index 1245b00..0000000 --- a/code/session-8/ConferencePlanner.sln +++ /dev/null @@ -1,48 +0,0 @@ - -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}") = "GraphQL", "GraphQL\GraphQL.csproj", "{48385280-56F1-4937-9655-E6A79184740B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Tests", "GraphQL.Tests\GraphQL.Tests.csproj", "{C8A7F903-5B5B-4994-B0F0-70719FC24D49}" -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 - {48385280-56F1-4937-9655-E6A79184740B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x64.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x64.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x86.ActiveCfg = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Debug|x86.Build.0 = Debug|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|Any CPU.Build.0 = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x64.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x64.Build.0 = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x86.ActiveCfg = Release|Any CPU - {48385280-56F1-4937-9655-E6A79184740B}.Release|x86.Build.0 = Release|Any CPU - {C8A7F903-5B5B-4994-B0F0-70719FC24D49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C8A7F903-5B5B-4994-B0F0-70719FC24D49}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C8A7F903-5B5B-4994-B0F0-70719FC24D49}.Debug|x64.ActiveCfg = Debug|Any CPU - {C8A7F903-5B5B-4994-B0F0-70719FC24D49}.Debug|x64.Build.0 = Debug|Any CPU - {C8A7F903-5B5B-4994-B0F0-70719FC24D49}.Debug|x86.ActiveCfg = Debug|Any CPU - {C8A7F903-5B5B-4994-B0F0-70719FC24D49}.Debug|x86.Build.0 = Debug|Any CPU - {C8A7F903-5B5B-4994-B0F0-70719FC24D49}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C8A7F903-5B5B-4994-B0F0-70719FC24D49}.Release|Any CPU.Build.0 = Release|Any CPU - {C8A7F903-5B5B-4994-B0F0-70719FC24D49}.Release|x64.ActiveCfg = Release|Any CPU - {C8A7F903-5B5B-4994-B0F0-70719FC24D49}.Release|x64.Build.0 = Release|Any CPU - {C8A7F903-5B5B-4994-B0F0-70719FC24D49}.Release|x86.ActiveCfg = Release|Any CPU - {C8A7F903-5B5B-4994-B0F0-70719FC24D49}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/code/session-8/GraphQL.Tests/AttendeeTests.cs b/code/session-8/GraphQL.Tests/AttendeeTests.cs deleted file mode 100644 index 445e16f..0000000 --- a/code/session-8/GraphQL.Tests/AttendeeTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using ConferencePlanner.GraphQL; -using ConferencePlanner.GraphQL.Attendees; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.Sessions; -using ConferencePlanner.GraphQL.Speakers; -using ConferencePlanner.GraphQL.Tracks; -using ConferencePlanner.GraphQL.Types; -using HotChocolate; -using HotChocolate.Execution; -using Snapshooter.Xunit; -using Xunit; - -namespace GraphQL.Tests -{ - public class AttendeeTests - { - [Fact] - public async Task Attendee_Schema_Changed() - { - // arrange - // act - ISchema schema = await new ServiceCollection() - .AddPooledDbContextFactory( - options => options.UseInMemoryDatabase("Data Source=conferences.db")) - .AddGraphQL() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport() - .BuildSchemaAsync(); - - // assert - schema.Print().MatchSnapshot(); - } - - [Fact] - public async Task RegisterAttendee() - { - // arrange - IRequestExecutor executor = await new ServiceCollection() - .AddPooledDbContextFactory( - options => options.UseInMemoryDatabase("Data Source=conferences.db")) - .AddGraphQL() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport() - .BuildRequestExecutorAsync(); - - // act - IExecutionResult result = await executor.ExecuteAsync(@" - mutation RegisterAttendee { - registerAttendee( - input: { - emailAddress: ""michael@chillicream.com"" - firstName: ""michael"" - lastName: ""staib"" - userName: ""michael3"" - }) - { - attendee { - id - } - } - }"); - - // assert - result.ToJson().MatchSnapshot(); - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL.Tests/GraphQL.Tests.csproj b/code/session-8/GraphQL.Tests/GraphQL.Tests.csproj deleted file mode 100644 index 559c88b..0000000 --- a/code/session-8/GraphQL.Tests/GraphQL.Tests.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - net5.0 - - false - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/code/session-8/GraphQL.Tests/__snapshots__/AttendeeTests.Attendee_Schema_Changed.snap b/code/session-8/GraphQL.Tests/__snapshots__/AttendeeTests.Attendee_Schema_Changed.snap deleted file mode 100644 index a9b7b64..0000000 --- a/code/session-8/GraphQL.Tests/__snapshots__/AttendeeTests.Attendee_Schema_Changed.snap +++ /dev/null @@ -1,151 +0,0 @@ -schema { - query: Query - mutation: Mutation -} - -"The node interface is implemented by entities that have a global unique identifier." -interface Node { - id: ID! -} - -type Attendee implements Node { - id: ID! - sessions: [Session] - firstName: String! - lastName: String! - userName: String! - emailAddress: String -} - -"A connection to a list of items." -type AttendeeConnection { - "Information to aid in pagination." - pageInfo: PageInfo! - "A list of edges." - edges: [AttendeeEdge!] - "A flattened list of the nodes." - nodes: [Attendee!] -} - -"An edge in a connection." -type AttendeeEdge { - "A cursor for use in pagination." - cursor: String! - "The item at the end of the edge." - node: Attendee! -} - -type CheckInAttendeePayload { - session: Session - attendee: Attendee - errors: [UserError!] -} - -type Mutation { - registerAttendee(input: RegisterAttendeeInput!): RegisterAttendeePayload! - checkInAttendee(input: CheckInAttendeeInput!): CheckInAttendeePayload! -} - -"Information about pagination in a connection." -type PageInfo { - "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 - "When paginating forwards, the cursor to continue." - endCursor: String -} - -type Query { - node(id: ID!): Node - attendees(first: Int after: String last: Int before: String): AttendeeConnection - attendeeById(id: ID!): Attendee! - attendeesById(ids: [ID!]!): [Attendee!]! -} - -type RegisterAttendeePayload { - attendee: Attendee - errors: [UserError!] -} - -type Session implements Node { - id: ID! - speakers: [Speaker] - attendees: [Attendee] - track: Track - trackId: ID - title: String! - abstract: String - startTime: DateTime - endTime: DateTime - duration: TimeSpan! -} - -"A connection to a list of items." -type SessionConnection { - "Information to aid in pagination." - pageInfo: PageInfo! - "A list of edges." - edges: [SessionEdge!] - "A flattened list of the nodes." - nodes: [Session!] -} - -"An edge in a connection." -type SessionEdge { - "A cursor for use in pagination." - cursor: String! - "The item at the end of the edge." - node: Session! -} - -type Speaker implements Node { - id: ID! - sessions: [Session] - name: String! - bio: String - webSite: String -} - -type Track implements Node { - id: ID! - sessions(first: Int after: String last: Int before: String): SessionConnection - name: String! -} - -type UserError { - message: String! - code: String! -} - -input CheckInAttendeeInput { - sessionId: ID! - attendeeId: ID! -} - -input RegisterAttendeeInput { - firstName: String! - lastName: String! - userName: String! - emailAddress: String! -} - -"The `Boolean` scalar type represents `true` or `false`." -scalar Boolean - -"The `DateTime` scalar represents an ISO-8601 compliant date time type." -scalar DateTime - -"The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID." -scalar ID - -"The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1." -scalar Int - -"The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text." -scalar String - -"The `TimeSpan` scalar represents an ISO-8601 compliant duration type." -scalar TimeSpan diff --git a/code/session-8/GraphQL/Attendees/AttendeeMutations.cs b/code/session-8/GraphQL/Attendees/AttendeeMutations.cs deleted file mode 100644 index 81d1e02..0000000 --- a/code/session-8/GraphQL/Attendees/AttendeeMutations.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Subscriptions; - -namespace ConferencePlanner.GraphQL.Attendees -{ - [ExtendObjectType(Name = "Mutation")] - public class AttendeeMutations - { - [UseApplicationDbContext] - public async Task RegisterAttendeeAsync( - RegisterAttendeeInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - var attendee = new Attendee - { - FirstName = input.FirstName, - LastName = input.LastName, - UserName = input.UserName, - EmailAddress = input.EmailAddress - }; - - context.Attendees.Add(attendee); - - await context.SaveChangesAsync(cancellationToken); - - return new RegisterAttendeePayload(attendee); - } - - [UseApplicationDbContext] - public async Task CheckInAttendeeAsync( - CheckInAttendeeInput input, - [ScopedService] ApplicationDbContext context, - [Service] ITopicEventSender eventSender, - CancellationToken cancellationToken) - { - Attendee attendee = await context.Attendees.FirstOrDefaultAsync( - t => t.Id == input.AttendeeId, cancellationToken); - - if (attendee is null) - { - return new CheckInAttendeePayload( - new UserError("Attendee not found.", "ATTENDEE_NOT_FOUND")); - } - - attendee.SessionsAttendees.Add( - new SessionAttendee - { - SessionId = input.SessionId - }); - - await context.SaveChangesAsync(cancellationToken); - - await eventSender.SendAsync( - "OnAttendeeCheckedIn_" + input.SessionId, - input.AttendeeId, - cancellationToken); - - return new CheckInAttendeePayload(attendee, input.SessionId); - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Attendees/AttendeePayloadBase.cs b/code/session-8/GraphQL/Attendees/AttendeePayloadBase.cs deleted file mode 100644 index 4b98558..0000000 --- a/code/session-8/GraphQL/Attendees/AttendeePayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Attendees -{ - public class AttendeePayloadBase : Payload - { - protected AttendeePayloadBase(Attendee attendee) - { - Attendee = attendee; - } - - protected AttendeePayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Attendee? Attendee { get; } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Attendees/AttendeeQueries.cs b/code/session-8/GraphQL/Attendees/AttendeeQueries.cs deleted file mode 100644 index e877335..0000000 --- a/code/session-8/GraphQL/Attendees/AttendeeQueries.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Attendees -{ - [ExtendObjectType(Name = "Query")] - public class AttendeeQueries - { - [UseApplicationDbContext] - [UsePaging] - public IQueryable GetAttendees( - [ScopedService] ApplicationDbContext context) => - context.Attendees; - - public Task GetAttendeeByIdAsync( - [ID(nameof(Attendee))] int id, - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) => - attendeeById.LoadAsync(id, cancellationToken); - - public async Task> GetAttendeesByIdAsync( - [ID(nameof(Attendee))] int[] ids, - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) => - await attendeeById.LoadAsync(ids, cancellationToken); - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Attendees/AttendeeSubscriptions.cs b/code/session-8/GraphQL/Attendees/AttendeeSubscriptions.cs deleted file mode 100644 index 475098e..0000000 --- a/code/session-8/GraphQL/Attendees/AttendeeSubscriptions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Execution; -using HotChocolate.Subscriptions; -using HotChocolate.Types; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Attendees -{ - [ExtendObjectType(Name = "Subscription")] - public class AttendeeSubscriptions - { - [Subscribe(With = nameof(SubscribeToOnAttendeeCheckedInAsync))] - public SessionAttendeeCheckIn OnAttendeeCheckedIn( - [ID(nameof(Session))] int sessionId, - [EventMessage] int attendeeId, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - new SessionAttendeeCheckIn(attendeeId, sessionId); - - public async ValueTask> SubscribeToOnAttendeeCheckedInAsync( - int sessionId, - [Service] ITopicEventReceiver eventReceiver, - CancellationToken cancellationToken) => - await eventReceiver.SubscribeAsync( - "OnAttendeeCheckedIn_" + sessionId, cancellationToken); - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Attendees/CheckInAttendeeInput.cs b/code/session-8/GraphQL/Attendees/CheckInAttendeeInput.cs deleted file mode 100644 index 5464c22..0000000 --- a/code/session-8/GraphQL/Attendees/CheckInAttendeeInput.cs +++ /dev/null @@ -1,11 +0,0 @@ -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Attendees -{ - public record CheckInAttendeeInput( - [ID(nameof(Session))] - int SessionId, - [ID(nameof(Attendee))] - int AttendeeId); -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Attendees/CheckInAttendeePayload.cs b/code/session-8/GraphQL/Attendees/CheckInAttendeePayload.cs deleted file mode 100644 index 29e1276..0000000 --- a/code/session-8/GraphQL/Attendees/CheckInAttendeePayload.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; - -namespace ConferencePlanner.GraphQL.Attendees -{ - public class CheckInAttendeePayload : AttendeePayloadBase - { - private int? _sessionId; - - public CheckInAttendeePayload(Attendee attendee, int sessionId) - : base(attendee) - { - _sessionId = sessionId; - } - - public CheckInAttendeePayload(UserError error) - : base(new[] { error }) - { - } - - public async Task GetSessionAsync( - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - if (_sessionId.HasValue) - { - return await sessionById.LoadAsync(_sessionId.Value, cancellationToken); - } - - return null; - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Attendees/RegisterAttendeeInput.cs b/code/session-8/GraphQL/Attendees/RegisterAttendeeInput.cs deleted file mode 100644 index 1710f0b..0000000 --- a/code/session-8/GraphQL/Attendees/RegisterAttendeeInput.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ConferencePlanner.GraphQL.Attendees -{ - public record RegisterAttendeeInput( - string FirstName, - string LastName, - string UserName, - string EmailAddress); -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Attendees/RegisterAttendeePayload.cs b/code/session-8/GraphQL/Attendees/RegisterAttendeePayload.cs deleted file mode 100644 index a79e99a..0000000 --- a/code/session-8/GraphQL/Attendees/RegisterAttendeePayload.cs +++ /dev/null @@ -1,18 +0,0 @@ -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Attendees -{ - public class RegisterAttendeePayload : AttendeePayloadBase - { - public RegisterAttendeePayload(Attendee attendee) - : base(attendee) - { - } - - public RegisterAttendeePayload(UserError error) - : base(new[] { error }) - { - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Attendees/SessionAttendeeCheckIn.cs b/code/session-8/GraphQL/Attendees/SessionAttendeeCheckIn.cs deleted file mode 100644 index af3c161..0000000 --- a/code/session-8/GraphQL/Attendees/SessionAttendeeCheckIn.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Attendees -{ - public class SessionAttendeeCheckIn - { - public SessionAttendeeCheckIn(int attendeeId, int sessionId) - { - AttendeeId = attendeeId; - SessionId = sessionId; - } - - [ID(nameof(Attendee))] - public int AttendeeId { get; } - - [ID(nameof(Session))] - public int SessionId { get; } - - [UseApplicationDbContext] - public async Task CheckInCountAsync( - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - await context.Sessions - .Where(session => session.Id == SessionId) - .SelectMany(session => session.SessionAttendees) - .CountAsync(cancellationToken); - - public Task GetAttendeeAsync( - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) => - attendeeById.LoadAsync(AttendeeId, cancellationToken); - - public Task GetSessionAsync( - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - sessionById.LoadAsync(AttendeeId, cancellationToken); - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Common/Payload.cs b/code/session-8/GraphQL/Common/Payload.cs deleted file mode 100644 index e9d2839..0000000 --- a/code/session-8/GraphQL/Common/Payload.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace ConferencePlanner.GraphQL.Common -{ - public abstract class Payload - { - protected Payload(IReadOnlyList? errors = null) - { - Errors = errors; - } - - public IReadOnlyList? Errors { get; } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Common/UserError.cs b/code/session-8/GraphQL/Common/UserError.cs deleted file mode 100644 index 3d587dd..0000000 --- a/code/session-8/GraphQL/Common/UserError.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace ConferencePlanner.GraphQL.Common -{ - public class UserError - { - public UserError(string message, string code) - { - Message = message; - Code = code; - } - - public string Message { get; } - - public string Code { get; } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Data/ApplicationDbContext.cs b/code/session-8/GraphQL/Data/ApplicationDbContext.cs deleted file mode 100644 index bbd7dda..0000000 --- a/code/session-8/GraphQL/Data/ApplicationDbContext.cs +++ /dev/null @@ -1,38 +0,0 @@ - using Microsoft.EntityFrameworkCore; - - namespace ConferencePlanner.GraphQL.Data - { - public class ApplicationDbContext : DbContext - { - public ApplicationDbContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasIndex(a => a.UserName) - .IsUnique(); - - // Many-to-many: Session <-> Attendee - modelBuilder - .Entity() - .HasKey(ca => new { ca.SessionId, ca.AttendeeId }); - - // Many-to-many: Speaker <-> Session - modelBuilder - .Entity() - .HasKey(ss => new { ss.SessionId, ss.SpeakerId }); - } - - public DbSet Sessions { get; set; } = default!; - - public DbSet Tracks { get; set; } = default!; - - public DbSet Speakers { get; set; } = default!; - - public DbSet Attendees { get; set; } = default!; - } - } \ No newline at end of file diff --git a/code/session-8/GraphQL/Data/Attendee.cs b/code/session-8/GraphQL/Data/Attendee.cs deleted file mode 100644 index e3f9ab0..0000000 --- a/code/session-8/GraphQL/Data/Attendee.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace ConferencePlanner.GraphQL.Data -{ - public class Attendee - { - public int Id { get; set; } - - [Required] - [StringLength(200)] - public string? FirstName { get; set; } - - [Required] - [StringLength(200)] - public string? LastName { get; set; } - - [Required] - [StringLength(200)] - public string? UserName { get; set; } - - [StringLength(256)] - public string? EmailAddress { get; set; } - - public ICollection SessionsAttendees { get; set; } = - new List(); - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Data/Session.cs b/code/session-8/GraphQL/Data/Session.cs deleted file mode 100644 index b340977..0000000 --- a/code/session-8/GraphQL/Data/Session.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace ConferencePlanner.GraphQL.Data -{ - public class Session - { - public int Id { get; set; } - - [Required] - [StringLength(200)] - public string? Title { get; set; } - - [StringLength(4000)] - public string? Abstract { get; set; } - - public DateTimeOffset? StartTime { get; set; } - - public DateTimeOffset? EndTime { get; set; } - - // Bonus points to those who can figure out why this is written this way - public TimeSpan Duration => - EndTime?.Subtract(StartTime ?? EndTime ?? DateTimeOffset.MinValue) ?? - TimeSpan.Zero; - - public int? TrackId { get; set; } - - public ICollection SessionSpeakers { get; set; } = - new List(); - - public ICollection SessionAttendees { get; set; } = - new List(); - - public Track? Track { get; set; } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Data/SessionAttendee.cs b/code/session-8/GraphQL/Data/SessionAttendee.cs deleted file mode 100644 index 089c71a..0000000 --- a/code/session-8/GraphQL/Data/SessionAttendee.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace ConferencePlanner.GraphQL.Data -{ - public class SessionAttendee - { - public int SessionId { get; set; } - - public Session? Session { get; set; } - - public int AttendeeId { get; set; } - - public Attendee? Attendee { get; set; } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Data/SessionSpeaker.cs b/code/session-8/GraphQL/Data/SessionSpeaker.cs deleted file mode 100644 index ed83e86..0000000 --- a/code/session-8/GraphQL/Data/SessionSpeaker.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace ConferencePlanner.GraphQL.Data -{ - public class SessionSpeaker - { - public int SessionId { get; set; } - - public Session? Session { get; set; } - - public int SpeakerId { get; set; } - - public Speaker? Speaker { get; set; } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Data/Speaker.cs b/code/session-8/GraphQL/Data/Speaker.cs deleted file mode 100644 index 0943514..0000000 --- a/code/session-8/GraphQL/Data/Speaker.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace ConferencePlanner.GraphQL.Data -{ - public class Speaker - { - public int Id { get; set; } - - [Required] - [StringLength(200)] - public string? Name { get; set; } - - [StringLength(4000)] - public string? Bio { get; set; } - - [StringLength(1000)] - public string? WebSite { get; set; } - - public ICollection SessionSpeakers { get; set; } = - new List(); - } - } \ No newline at end of file diff --git a/code/session-8/GraphQL/Data/Track.cs b/code/session-8/GraphQL/Data/Track.cs deleted file mode 100644 index f2392b6..0000000 --- a/code/session-8/GraphQL/Data/Track.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace ConferencePlanner.GraphQL.Data -{ - public class Track - { - public int Id { get; set; } - - [Required] - [StringLength(200)] - public string? Name { get; set; } - - public ICollection Sessions { get; set; } = - new List(); - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/DataLoader/AttendeeByIdDataLoader.cs b/code/session-8/GraphQL/DataLoader/AttendeeByIdDataLoader.cs deleted file mode 100644 index a2bdbd0..0000000 --- a/code/session-8/GraphQL/DataLoader/AttendeeByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class AttendeeByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public AttendeeByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Attendees - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/DataLoader/SessionByIdDataLoader.cs b/code/session-8/GraphQL/DataLoader/SessionByIdDataLoader.cs deleted file mode 100644 index dbd675b..0000000 --- a/code/session-8/GraphQL/DataLoader/SessionByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class SessionByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public SessionByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Sessions - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/DataLoader/SpeakerByIdDataLoader.cs b/code/session-8/GraphQL/DataLoader/SpeakerByIdDataLoader.cs deleted file mode 100644 index 44d8208..0000000 --- a/code/session-8/GraphQL/DataLoader/SpeakerByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class SpeakerByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public SpeakerByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Speakers - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/DataLoader/TrackByIdDataLoader.cs b/code/session-8/GraphQL/DataLoader/TrackByIdDataLoader.cs deleted file mode 100644 index 4db1f95..0000000 --- a/code/session-8/GraphQL/DataLoader/TrackByIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using GreenDonut; -using HotChocolate.DataLoader; - -namespace ConferencePlanner.GraphQL.DataLoader -{ - public class TrackByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public TrackByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Tracks - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs b/code/session-8/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs deleted file mode 100644 index 370c767..0000000 --- a/code/session-8/GraphQL/Extensions/ObjectFieldDescriptorExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL -{ - public static class ObjectFieldDescriptorExtensions - { - public static IObjectFieldDescriptor UseDbContext( - this IObjectFieldDescriptor descriptor) - where TDbContext : DbContext - { - return descriptor.UseScopedService( - create: s => s.GetRequiredService>().CreateDbContext(), - disposeAsync: (s, c) => c.DisposeAsync()); - } - - public static IObjectFieldDescriptor UseUpperCase( - this IObjectFieldDescriptor descriptor) - { - return descriptor.Use(next => async context => - { - await next(context); - - if (context.Result is string s) - { - context.Result = s.ToUpperInvariant(); - } - }); - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Extensions/UseApplicationDbContextAttribute.cs b/code/session-8/GraphQL/Extensions/UseApplicationDbContextAttribute.cs deleted file mode 100644 index 79c9907..0000000 --- a/code/session-8/GraphQL/Extensions/UseApplicationDbContextAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Reflection; -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types; -using HotChocolate.Types.Descriptors; - -namespace ConferencePlanner.GraphQL -{ - public class UseApplicationDbContextAttribute : ObjectFieldDescriptorAttribute - { - public override void OnConfigure( - IDescriptorContext context, - IObjectFieldDescriptor descriptor, - MemberInfo member) - { - descriptor.UseDbContext(); - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Extensions/UseUpperCaseAttribute.cs b/code/session-8/GraphQL/Extensions/UseUpperCaseAttribute.cs deleted file mode 100644 index 8376b5b..0000000 --- a/code/session-8/GraphQL/Extensions/UseUpperCaseAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -using HotChocolate.Types; -using HotChocolate.Types.Descriptors; -using System.Reflection; - -namespace ConferencePlanner.GraphQL -{ - public class UseUpperCaseAttribute : ObjectFieldDescriptorAttribute - { - public override void OnConfigure( - IDescriptorContext context, - IObjectFieldDescriptor descriptor, - MemberInfo member) - { - descriptor.UseUpperCase(); - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/GraphQL.csproj b/code/session-8/GraphQL/GraphQL.csproj deleted file mode 100644 index c8e2571..0000000 --- a/code/session-8/GraphQL/GraphQL.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net5.0 - ConferencePlanner.GraphQL - enable - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - diff --git a/code/session-8/GraphQL/Migrations/20201010183502_Initial.Designer.cs b/code/session-8/GraphQL/Migrations/20201010183502_Initial.Designer.cs deleted file mode 100644 index dc170c1..0000000 --- a/code/session-8/GraphQL/Migrations/20201010183502_Initial.Designer.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -using ConferencePlanner.GraphQL.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace GraphQL.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20201010183502_Initial")] - partial class Initial - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bio") - .HasMaxLength(4000) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("WebSite") - .HasMaxLength(1000) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Speakers"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/code/session-8/GraphQL/Migrations/20201010183502_Initial.cs b/code/session-8/GraphQL/Migrations/20201010183502_Initial.cs deleted file mode 100644 index 69e30fb..0000000 --- a/code/session-8/GraphQL/Migrations/20201010183502_Initial.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace GraphQL.Migrations -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Speakers", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), - Bio = table.Column(type: "TEXT", maxLength: 4000, nullable: true), - WebSite = table.Column(type: "TEXT", maxLength: 1000, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Speakers", x => x.Id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Speakers"); - } - } -} diff --git a/code/session-8/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs b/code/session-8/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs deleted file mode 100644 index 2e3d723..0000000 --- a/code/session-8/GraphQL/Migrations/20201010202211_Refactoring.Designer.cs +++ /dev/null @@ -1,226 +0,0 @@ -// -using System; -using ConferencePlanner.GraphQL.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace GraphQL.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20201010202211_Refactoring")] - partial class Refactoring - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("EmailAddress") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("FirstName") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("LastName") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserName") - .IsUnique(); - - b.ToTable("Attendees"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Abstract") - .HasMaxLength(4000) - .HasColumnType("TEXT"); - - b.Property("EndTime") - .HasColumnType("TEXT"); - - b.Property("StartTime") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("TrackId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TrackId"); - - b.ToTable("Sessions"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => - { - b.Property("SessionId") - .HasColumnType("INTEGER"); - - b.Property("AttendeeId") - .HasColumnType("INTEGER"); - - b.HasKey("SessionId", "AttendeeId"); - - b.HasIndex("AttendeeId"); - - b.ToTable("SessionAttendee"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => - { - b.Property("SessionId") - .HasColumnType("INTEGER"); - - b.Property("SpeakerId") - .HasColumnType("INTEGER"); - - b.HasKey("SessionId", "SpeakerId"); - - b.HasIndex("SpeakerId"); - - b.ToTable("SessionSpeaker"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bio") - .HasMaxLength(4000) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("WebSite") - .HasMaxLength(1000) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Speakers"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Track", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Tracks"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => - { - b.HasOne("ConferencePlanner.GraphQL.Data.Track", "Track") - .WithMany("Sessions") - .HasForeignKey("TrackId"); - - b.Navigation("Track"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => - { - b.HasOne("ConferencePlanner.GraphQL.Data.Attendee", "Attendee") - .WithMany("SessionsAttendees") - .HasForeignKey("AttendeeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("ConferencePlanner.GraphQL.Data.Session", "Session") - .WithMany("SessionAttendees") - .HasForeignKey("SessionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Attendee"); - - b.Navigation("Session"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => - { - b.HasOne("ConferencePlanner.GraphQL.Data.Session", "Session") - .WithMany("SessionSpeakers") - .HasForeignKey("SessionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("ConferencePlanner.GraphQL.Data.Speaker", "Speaker") - .WithMany("SessionSpeakers") - .HasForeignKey("SpeakerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Session"); - - b.Navigation("Speaker"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => - { - b.Navigation("SessionsAttendees"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => - { - b.Navigation("SessionAttendees"); - - b.Navigation("SessionSpeakers"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => - { - b.Navigation("SessionSpeakers"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Track", b => - { - b.Navigation("Sessions"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/code/session-8/GraphQL/Migrations/20201010202211_Refactoring.cs b/code/session-8/GraphQL/Migrations/20201010202211_Refactoring.cs deleted file mode 100644 index ffdcfeb..0000000 --- a/code/session-8/GraphQL/Migrations/20201010202211_Refactoring.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace GraphQL.Migrations -{ - public partial class Refactoring : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Attendees", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - FirstName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - LastName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - UserName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - EmailAddress = table.Column(type: "TEXT", maxLength: 256, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Attendees", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Tracks", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", maxLength: 200, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Tracks", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Sessions", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Title = table.Column(type: "TEXT", maxLength: 200, nullable: false), - Abstract = table.Column(type: "TEXT", maxLength: 4000, nullable: true), - StartTime = table.Column(type: "TEXT", nullable: true), - EndTime = table.Column(type: "TEXT", nullable: true), - TrackId = table.Column(type: "INTEGER", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Sessions", x => x.Id); - table.ForeignKey( - name: "FK_Sessions_Tracks_TrackId", - column: x => x.TrackId, - principalTable: "Tracks", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "SessionAttendee", - columns: table => new - { - SessionId = table.Column(type: "INTEGER", nullable: false), - AttendeeId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_SessionAttendee", x => new { x.SessionId, x.AttendeeId }); - table.ForeignKey( - name: "FK_SessionAttendee_Attendees_AttendeeId", - column: x => x.AttendeeId, - principalTable: "Attendees", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_SessionAttendee_Sessions_SessionId", - column: x => x.SessionId, - principalTable: "Sessions", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "SessionSpeaker", - columns: table => new - { - SessionId = table.Column(type: "INTEGER", nullable: false), - SpeakerId = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_SessionSpeaker", x => new { x.SessionId, x.SpeakerId }); - table.ForeignKey( - name: "FK_SessionSpeaker_Sessions_SessionId", - column: x => x.SessionId, - principalTable: "Sessions", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_SessionSpeaker_Speakers_SpeakerId", - column: x => x.SpeakerId, - principalTable: "Speakers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Attendees_UserName", - table: "Attendees", - column: "UserName", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_SessionAttendee_AttendeeId", - table: "SessionAttendee", - column: "AttendeeId"); - - migrationBuilder.CreateIndex( - name: "IX_Sessions_TrackId", - table: "Sessions", - column: "TrackId"); - - migrationBuilder.CreateIndex( - name: "IX_SessionSpeaker_SpeakerId", - table: "SessionSpeaker", - column: "SpeakerId"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "SessionAttendee"); - - migrationBuilder.DropTable( - name: "SessionSpeaker"); - - migrationBuilder.DropTable( - name: "Attendees"); - - migrationBuilder.DropTable( - name: "Sessions"); - - migrationBuilder.DropTable( - name: "Tracks"); - } - } -} diff --git a/code/session-8/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs b/code/session-8/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs deleted file mode 100644 index a66dfe1..0000000 --- a/code/session-8/GraphQL/Migrations/ApplicationDbContextModelSnapshot.cs +++ /dev/null @@ -1,224 +0,0 @@ -// -using System; -using ConferencePlanner.GraphQL.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace GraphQL.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - partial class ApplicationDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "5.0.0"); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("EmailAddress") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("FirstName") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("LastName") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("UserName") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserName") - .IsUnique(); - - b.ToTable("Attendees"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Abstract") - .HasMaxLength(4000) - .HasColumnType("TEXT"); - - b.Property("EndTime") - .HasColumnType("TEXT"); - - b.Property("StartTime") - .HasColumnType("TEXT"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("TrackId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("TrackId"); - - b.ToTable("Sessions"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => - { - b.Property("SessionId") - .HasColumnType("INTEGER"); - - b.Property("AttendeeId") - .HasColumnType("INTEGER"); - - b.HasKey("SessionId", "AttendeeId"); - - b.HasIndex("AttendeeId"); - - b.ToTable("SessionAttendee"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => - { - b.Property("SessionId") - .HasColumnType("INTEGER"); - - b.Property("SpeakerId") - .HasColumnType("INTEGER"); - - b.HasKey("SessionId", "SpeakerId"); - - b.HasIndex("SpeakerId"); - - b.ToTable("SessionSpeaker"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Bio") - .HasMaxLength(4000) - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.Property("WebSite") - .HasMaxLength(1000) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Speakers"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Track", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Tracks"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => - { - b.HasOne("ConferencePlanner.GraphQL.Data.Track", "Track") - .WithMany("Sessions") - .HasForeignKey("TrackId"); - - b.Navigation("Track"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionAttendee", b => - { - b.HasOne("ConferencePlanner.GraphQL.Data.Attendee", "Attendee") - .WithMany("SessionsAttendees") - .HasForeignKey("AttendeeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("ConferencePlanner.GraphQL.Data.Session", "Session") - .WithMany("SessionAttendees") - .HasForeignKey("SessionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Attendee"); - - b.Navigation("Session"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.SessionSpeaker", b => - { - b.HasOne("ConferencePlanner.GraphQL.Data.Session", "Session") - .WithMany("SessionSpeakers") - .HasForeignKey("SessionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("ConferencePlanner.GraphQL.Data.Speaker", "Speaker") - .WithMany("SessionSpeakers") - .HasForeignKey("SpeakerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Session"); - - b.Navigation("Speaker"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Attendee", b => - { - b.Navigation("SessionsAttendees"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Session", b => - { - b.Navigation("SessionAttendees"); - - b.Navigation("SessionSpeakers"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Speaker", b => - { - b.Navigation("SessionSpeakers"); - }); - - modelBuilder.Entity("ConferencePlanner.GraphQL.Data.Track", b => - { - b.Navigation("Sessions"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/code/session-8/GraphQL/Program.cs b/code/session-8/GraphQL/Program.cs deleted file mode 100644 index c4914c6..0000000 --- a/code/session-8/GraphQL/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -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 ConferencePlanner.GraphQL -{ - 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/code/session-8/GraphQL/Sessions/AddSessionInput.cs b/code/session-8/GraphQL/Sessions/AddSessionInput.cs deleted file mode 100644 index db5995f..0000000 --- a/code/session-8/GraphQL/Sessions/AddSessionInput.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public record AddSessionInput( - string Title, - string? Abstract, - [ID(nameof(Speaker))] - IReadOnlyList SpeakerIds); -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Sessions/AddSessionPayload.cs b/code/session-8/GraphQL/Sessions/AddSessionPayload.cs deleted file mode 100644 index 82775f8..0000000 --- a/code/session-8/GraphQL/Sessions/AddSessionPayload.cs +++ /dev/null @@ -1,20 +0,0 @@ -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class AddSessionPayload : Payload - { - public AddSessionPayload(Session session) - { - Session = session; - } - - public AddSessionPayload(UserError error) - : base(new[] { error }) - { - } - - public Session? Session { get; init; } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Sessions/ScheduleSessionInput.cs b/code/session-8/GraphQL/Sessions/ScheduleSessionInput.cs deleted file mode 100644 index fc43463..0000000 --- a/code/session-8/GraphQL/Sessions/ScheduleSessionInput.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public record ScheduleSessionInput( - [ID(nameof(Session))] - int SessionId, - [ID(nameof(Track))] - int TrackId, - DateTimeOffset StartTime, - DateTimeOffset EndTime); -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Sessions/ScheduleSessionPayload.cs b/code/session-8/GraphQL/Sessions/ScheduleSessionPayload.cs deleted file mode 100644 index ce79df5..0000000 --- a/code/session-8/GraphQL/Sessions/ScheduleSessionPayload.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class ScheduleSessionPayload : SessionPayloadBase - { - public ScheduleSessionPayload(Session session) - : base(session) - { - } - - public ScheduleSessionPayload(UserError error) - : base(new[] { error }) - { - } - - public async Task GetTrackAsync( - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) - { - if (Session is null) - { - return null; - } - - return await trackById.LoadAsync(Session.Id, cancellationToken); - } - - [UseApplicationDbContext] - public async Task?> GetSpeakersAsync( - [ScopedService] ApplicationDbContext dbContext, - SpeakerByIdDataLoader speakerById, - CancellationToken cancellationToken) - { - if (Session is null) - { - return null; - } - - int[] speakerIds = await dbContext.Sessions - .Where(s => s.Id == Session.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SpeakerId)) - .ToArrayAsync(); - - return await speakerById.LoadAsync(speakerIds, cancellationToken); - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Sessions/SessionMutations.cs b/code/session-8/GraphQL/Sessions/SessionMutations.cs deleted file mode 100644 index ac6148f..0000000 --- a/code/session-8/GraphQL/Sessions/SessionMutations.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Subscriptions; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Sessions -{ - [ExtendObjectType(Name = "Mutation")] - public class SessionMutations - { - [UseApplicationDbContext] - public async Task AddSessionAsync( - AddSessionInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(input.Title)) - { - return new AddSessionPayload( - new UserError("The title cannot be empty.", "TITLE_EMPTY")); - } - - if (input.SpeakerIds.Count == 0) - { - return new AddSessionPayload( - new UserError("No speaker assigned.", "NO_SPEAKER")); - } - - var session = new Session - { - Title = input.Title, - Abstract = input.Abstract, - }; - - foreach (int speakerId in input.SpeakerIds) - { - session.SessionSpeakers.Add(new SessionSpeaker - { - SpeakerId = speakerId - }); - } - - context.Sessions.Add(session); - await context.SaveChangesAsync(cancellationToken); - - return new AddSessionPayload(session); - } - - [UseApplicationDbContext] - public async Task ScheduleSessionAsync( - ScheduleSessionInput input, - [ScopedService] ApplicationDbContext context, - [Service]ITopicEventSender eventSender) - { - if (input.EndTime < input.StartTime) - { - return new ScheduleSessionPayload( - new UserError("endTime has to be larger than startTime.", "END_TIME_INVALID")); - } - - Session session = await context.Sessions.FindAsync(input.SessionId); - int? initialTrackId = session.TrackId; - - if (session is null) - { - return new ScheduleSessionPayload( - new UserError("Session not found.", "SESSION_NOT_FOUND")); - } - - session.TrackId = input.TrackId; - session.StartTime = input.StartTime; - session.EndTime = input.EndTime; - - await context.SaveChangesAsync(); - - await eventSender.SendAsync( - nameof(SessionSubscriptions.OnSessionScheduledAsync), - session.Id); - - return new ScheduleSessionPayload(session); - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Sessions/SessionPayloadBase.cs b/code/session-8/GraphQL/Sessions/SessionPayloadBase.cs deleted file mode 100644 index 888ad50..0000000 --- a/code/session-8/GraphQL/Sessions/SessionPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Sessions -{ - public class SessionPayloadBase : Payload - { - protected SessionPayloadBase(Session session) - { - Session = session; - } - - protected SessionPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Session? Session { get; } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Sessions/SessionQueries.cs b/code/session-8/GraphQL/Sessions/SessionQueries.cs deleted file mode 100644 index 1cacdfd..0000000 --- a/code/session-8/GraphQL/Sessions/SessionQueries.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; -using System.Linq; -using ConferencePlanner.GraphQL.Types; -using HotChocolate.Data; - -namespace ConferencePlanner.GraphQL.Sessions -{ - [ExtendObjectType(Name = "Query")] - public class SessionQueries - { - [UseApplicationDbContext] - [UsePaging(typeof(NonNullType))] - // TODO: [UseFiltering(typeof(SessionFilterInputType))] - [UseFiltering] - [UseSorting] - public IQueryable GetSessions( - [ScopedService] ApplicationDbContext context) => - context.Sessions; - - public Task GetSessionByIdAsync( - [ID(nameof(Session))] int id, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - sessionById.LoadAsync(id, cancellationToken); - - public async Task> GetSessionsByIdAsync( - [ID(nameof(Session))] int[] ids, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - await sessionById.LoadAsync(ids, cancellationToken); - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Sessions/SessionSubscriptions.cs b/code/session-8/GraphQL/Sessions/SessionSubscriptions.cs deleted file mode 100644 index fef037a..0000000 --- a/code/session-8/GraphQL/Sessions/SessionSubscriptions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Sessions -{ - [ExtendObjectType(Name = "Subscription")] - public class SessionSubscriptions - { - [Subscribe] - [Topic] - public Task OnSessionScheduledAsync( - [EventMessage] int sessionId, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - sessionById.LoadAsync(sessionId, cancellationToken); - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Speakers/AddSpeakerInput.cs b/code/session-8/GraphQL/Speakers/AddSpeakerInput.cs deleted file mode 100644 index a81f45f..0000000 --- a/code/session-8/GraphQL/Speakers/AddSpeakerInput.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ConferencePlanner.GraphQL.Speakers -{ - public record AddSpeakerInput( - string Name, - string? Bio, - string? WebSite); -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Speakers/AddSpeakerPayload.cs b/code/session-8/GraphQL/Speakers/AddSpeakerPayload.cs deleted file mode 100644 index aaf0ab0..0000000 --- a/code/session-8/GraphQL/Speakers/AddSpeakerPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Speakers -{ - public class AddSpeakerPayload : SpeakerPayloadBase - { - public AddSpeakerPayload(Speaker speaker) - : base(speaker) - { - } - - public AddSpeakerPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Speakers/SpeakerMutations.cs b/code/session-8/GraphQL/Speakers/SpeakerMutations.cs deleted file mode 100644 index cb7f4c5..0000000 --- a/code/session-8/GraphQL/Speakers/SpeakerMutations.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Speakers -{ - [ExtendObjectType("Mutation")] - public class SpeakerMutations - { - [UseApplicationDbContext] - public async Task AddSpeakerAsync( - AddSpeakerInput input, - [ScopedService] ApplicationDbContext context) - { - var speaker = new Speaker - { - Name = input.Name, - Bio = input.Bio, - WebSite = input.WebSite - }; - - context.Speakers.Add(speaker); - await context.SaveChangesAsync(); - - return new AddSpeakerPayload(speaker); - } - } -} diff --git a/code/session-8/GraphQL/Speakers/SpeakerPayloadBase.cs b/code/session-8/GraphQL/Speakers/SpeakerPayloadBase.cs deleted file mode 100644 index a2077d7..0000000 --- a/code/session-8/GraphQL/Speakers/SpeakerPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Speakers -{ - public class SpeakerPayloadBase : Payload - { - protected SpeakerPayloadBase(Speaker speaker) - { - Speaker = speaker; - } - - protected SpeakerPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Speaker? Speaker { get; init; } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Speakers/SpeakerQueries.cs b/code/session-8/GraphQL/Speakers/SpeakerQueries.cs deleted file mode 100644 index c9b57b8..0000000 --- a/code/session-8/GraphQL/Speakers/SpeakerQueries.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; -using System.Linq; - -namespace ConferencePlanner.GraphQL.Speakers -{ - [ExtendObjectType(Name = "Query")] - public class SpeakerQueries - { - [UseApplicationDbContext] - [UsePaging] - public IQueryable GetSpeakers( - [ScopedService] ApplicationDbContext context) => - context.Speakers.OrderBy(t => t.Name); - - public Task GetSpeakerByIdAsync( - [ID(nameof(Speaker))]int id, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - dataLoader.LoadAsync(id, cancellationToken); - - public async Task> GetSpeakersByIdAsync( - [ID(nameof(Speaker))]int[] ids, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - await dataLoader.LoadAsync(ids, cancellationToken); - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Startup.cs b/code/session-8/GraphQL/Startup.cs deleted file mode 100644 index efcc20a..0000000 --- a/code/session-8/GraphQL/Startup.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL; -using ConferencePlanner.GraphQL.Attendees; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using ConferencePlanner.GraphQL.Sessions; -using ConferencePlanner.GraphQL.Speakers; -using ConferencePlanner.GraphQL.Tracks; -using ConferencePlanner.GraphQL.Types; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace ConferencePlanner.GraphQL -{ - 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.AddPooledDbContextFactory( - options => options.UseSqlite("Data Source=conferences.db")); - - services - .AddGraphQLServer() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddSubscriptionType(d => d.Name("Subscription")) - .AddTypeExtension() - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport() - .AddFiltering() - .AddSorting() - .AddInMemorySubscriptions() - .AddDataLoader() - .AddDataLoader(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseWebSockets(); - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGraphQL(); - }); - } - } -} diff --git a/code/session-8/GraphQL/Tracks/AddTrackInput.cs b/code/session-8/GraphQL/Tracks/AddTrackInput.cs deleted file mode 100644 index 5c83b34..0000000 --- a/code/session-8/GraphQL/Tracks/AddTrackInput.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace ConferencePlanner.GraphQL.Tracks -{ - public record AddTrackInput(string Name); -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Tracks/AddTrackPayload.cs b/code/session-8/GraphQL/Tracks/AddTrackPayload.cs deleted file mode 100644 index 8f35b13..0000000 --- a/code/session-8/GraphQL/Tracks/AddTrackPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class AddTrackPayload : TrackPayloadBase - { - public AddTrackPayload(Track track) - : base(track) - { - } - - public AddTrackPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Tracks/RenameTrackInput.cs b/code/session-8/GraphQL/Tracks/RenameTrackInput.cs deleted file mode 100644 index 516c6a0..0000000 --- a/code/session-8/GraphQL/Tracks/RenameTrackInput.cs +++ /dev/null @@ -1,7 +0,0 @@ -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public record RenameTrackInput([ID(nameof(Track))] int Id, string Name); -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Tracks/RenameTrackPayload.cs b/code/session-8/GraphQL/Tracks/RenameTrackPayload.cs deleted file mode 100644 index ca4c8a1..0000000 --- a/code/session-8/GraphQL/Tracks/RenameTrackPayload.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class RenameTrackPayload : TrackPayloadBase - { - public RenameTrackPayload(Track track) - : base(track) - { - } - - public RenameTrackPayload(IReadOnlyList errors) - : base(errors) - { - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Tracks/TrackMutations.cs b/code/session-8/GraphQL/Tracks/TrackMutations.cs deleted file mode 100644 index 88727b9..0000000 --- a/code/session-8/GraphQL/Tracks/TrackMutations.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ConferencePlanner.GraphQL.Data; -using HotChocolate; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Tracks -{ - [ExtendObjectType(Name = "Mutation")] - public class TrackMutations - { - [UseApplicationDbContext] - public async Task AddTrackAsync( - AddTrackInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - var track = new Track { Name = input.Name }; - context.Tracks.Add(track); - - await context.SaveChangesAsync(cancellationToken); - - return new AddTrackPayload(track); - } - - [UseApplicationDbContext] - public async Task RenameTrackAsync( - RenameTrackInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - Track track = await context.Tracks.FindAsync(input.Id); - track.Name = input.Name; - - await context.SaveChangesAsync(cancellationToken); - - return new RenameTrackPayload(track); - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Tracks/TrackPayloadBase.cs b/code/session-8/GraphQL/Tracks/TrackPayloadBase.cs deleted file mode 100644 index de11da7..0000000 --- a/code/session-8/GraphQL/Tracks/TrackPayloadBase.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using ConferencePlanner.GraphQL.Common; -using ConferencePlanner.GraphQL.Data; - -namespace ConferencePlanner.GraphQL.Tracks -{ - public class TrackPayloadBase : Payload - { - public TrackPayloadBase(Track track) - { - Track = track; - } - - public TrackPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Track? Track { get; } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Tracks/TrackQueries.cs b/code/session-8/GraphQL/Tracks/TrackQueries.cs deleted file mode 100644 index 7ced249..0000000 --- a/code/session-8/GraphQL/Tracks/TrackQueries.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Types; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Tracks -{ - [ExtendObjectType(Name = "Query")] - public class TrackQueries - { - [UseApplicationDbContext] - [UsePaging] - public IQueryable GetTracks( - [ScopedService] ApplicationDbContext context) => - context.Tracks.OrderBy(t => t.Name); - - [UseApplicationDbContext] - public Task GetTrackByNameAsync( - string name, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - context.Tracks.FirstAsync(t => t.Name == name); - - [UseApplicationDbContext] - public async Task> GetTrackByNamesAsync( - string[] names, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - await context.Tracks.Where(t => names.Contains(t.Name)).ToListAsync(); - - public Task GetTrackByIdAsync( - [ID(nameof(Track))] int id, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) => - trackById.LoadAsync(id, cancellationToken); - - public async Task> GetTracksByIdAsync( - [ID(nameof(Track))] int[] ids, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) => - await trackById.LoadAsync(ids, cancellationToken); - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Types/AttendeeType.cs b/code/session-8/GraphQL/Types/AttendeeType.cs deleted file mode 100644 index 1286503..0000000 --- a/code/session-8/GraphQL/Types/AttendeeType.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Types -{ - public class AttendeeType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionsAttendees) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("sessions"); - } - - private class AttendeeResolvers - { - public async Task> GetSessionsAsync( - Attendee attendee, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] sessionIds = await dbContext.Attendees - .Where(a => a.Id == attendee.Id) - .Include(a => a.SessionsAttendees) - .SelectMany(a => a.SessionsAttendees.Select(t => t.SessionId)) - .ToArrayAsync(); - - return await sessionById.LoadAsync(sessionIds, cancellationToken); - } - } - } -} diff --git a/code/session-8/GraphQL/Types/SessionFilterInputType.cs b/code/session-8/GraphQL/Types/SessionFilterInputType.cs deleted file mode 100644 index 9a514e8..0000000 --- a/code/session-8/GraphQL/Types/SessionFilterInputType.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ConferencePlanner.GraphQL.Data; -using HotChocolate.Data.Filters; - -namespace ConferencePlanner.GraphQL.Types -{ - public class SessionFilterInputType : FilterInputType - { - protected override void Configure(IFilterInputTypeDescriptor descriptor) - { - descriptor.Ignore(t => t.Id); - descriptor.Ignore(t => t.TrackId); - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Types/SessionType.cs b/code/session-8/GraphQL/Types/SessionType.cs deleted file mode 100644 index 8a558f7..0000000 --- a/code/session-8/GraphQL/Types/SessionType.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; -using HotChocolate.Types.Relay; - -namespace ConferencePlanner.GraphQL.Types -{ - public class SessionType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionSpeakers) - .ResolveWith(t => t.GetSpeakersAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("speakers"); - - descriptor - .Field(t => t.SessionAttendees) - .ResolveWith(t => t.GetAttendeesAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("attendees"); - - descriptor - .Field(t => t.Track) - .ResolveWith(t => t.GetTrackAsync(default!, default!, default)); - - descriptor - .Field(t => t.TrackId) - .ID(nameof(Track)); - } - - private class SessionResolvers - { - public async Task> GetSpeakersAsync( - Session session, - [ScopedService] ApplicationDbContext dbContext, - SpeakerByIdDataLoader speakerById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Sessions - .Where(s => s.Id == session.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SpeakerId)) - .ToArrayAsync(); - - return await speakerById.LoadAsync(speakerIds, cancellationToken); - } - - public async Task> GetAttendeesAsync( - Session session, - [ScopedService] ApplicationDbContext dbContext, - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) - { - int[] attendeeIds = await dbContext.Sessions - .Where(s => s.Id == session.Id) - .Include(session => session.SessionAttendees) - .SelectMany(session => session.SessionAttendees.Select(t => t.AttendeeId)) - .ToArrayAsync(); - - return await attendeeById.LoadAsync(attendeeIds, cancellationToken); - } - - public async Task GetTrackAsync( - Session session, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) - { - if (session.TrackId is null) - { - return null; - } - - return await trackById.LoadAsync(session.TrackId.Value, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Types/SpeakerType.cs b/code/session-8/GraphQL/Types/SpeakerType.cs deleted file mode 100644 index 89837f0..0000000 --- a/code/session-8/GraphQL/Types/SpeakerType.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Types -{ - public class SpeakerType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionSpeakers) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("sessions"); - } - - private class SpeakerResolvers - { - public async Task> GetSessionsAsync( - Speaker speaker, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Speakers - .Where(s => s.Id == speaker.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SessionId)) - .ToArrayAsync(); - - return await sessionById.LoadAsync(speakerIds, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/Types/TrackType.cs b/code/session-8/GraphQL/Types/TrackType.cs deleted file mode 100644 index dfa4faf..0000000 --- a/code/session-8/GraphQL/Types/TrackType.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using ConferencePlanner.GraphQL.Data; -using ConferencePlanner.GraphQL.DataLoader; -using HotChocolate; -using HotChocolate.Resolvers; -using HotChocolate.Types; - -namespace ConferencePlanner.GraphQL.Types -{ - public class TrackType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => - ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.Sessions) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .UsePaging>() - .Name("sessions"); - - descriptor - .Field(t => t.Name) - .UseUpperCase(); - } - - private class TrackResolvers - { - public async Task> GetSessionsAsync( - Track track, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] sessionIds = await dbContext.Sessions - .Where(s => s.Id == track.Id) - .Select(s => s.Id) - .ToArrayAsync(); - - return await sessionById.LoadAsync(sessionIds, cancellationToken); - } - } - } -} \ No newline at end of file diff --git a/code/session-8/GraphQL/appsettings.Development.json b/code/session-8/GraphQL/appsettings.Development.json deleted file mode 100644 index dba68eb..0000000 --- a/code/session-8/GraphQL/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/code/session-8/GraphQL/appsettings.json b/code/session-8/GraphQL/appsettings.json deleted file mode 100644 index 81ff877..0000000 --- a/code/session-8/GraphQL/appsettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} diff --git a/cspell.json b/cspell.json new file mode 100644 index 0000000..198bc94 --- /dev/null +++ b/cspell.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", + "version": "0.2", + "language": "en", + "dictionaries": ["csharp", "custom"], + "dictionaryDefinitions": [ + { + "name": "custom", + "path": "./dictionary.txt" + } + ], + "ignorePaths": [ + "code/complete/GraphQL/Imports/NDC_London_2019.json" + ] +} diff --git a/dictionary.txt b/dictionary.txt new file mode 100644 index 0000000..9f0d134 --- /dev/null +++ b/dictionary.txt @@ -0,0 +1,17 @@ +aspnet +ASPNETCORE +Autoincrement +autonumber +buildtransitive +contentfiles +deprioritization +Linq +ncontains +nendsWith +ngte +nlte +Npgsql +nstartsWith +staib +Testcontainers +xunit diff --git a/docs/1-creating-a-graphql-server-project.md b/docs/1-creating-a-graphql-server-project.md index 7aa4eca..ffa0cdf 100644 --- a/docs/1-creating-a-graphql-server-project.md +++ b/docs/1-creating-a-graphql-server-project.md @@ -1,321 +1,357 @@ -- [Create a new GraphQL server project](#create-a-new-graphql-server-project) - - [Register the DB Context Service](#register-the-db-context-service) - - [Configuring EF Migrations](#configuring-ef-migrations) - - [Option 1 - Visual Studio: Package Manager Console](#option-1---visual-studio-package-manager-console) - - [Option 2 - Command line](#option-2---command-line) - - [Adding GraphQL](#adding-graphql) - - [Adding Mutations](#adding-mutations) - - [Summary](#summary) -# Create a new GraphQL server project - -1. Create a new project for our GraphQL Server. - 1. `dotnet new sln -n ConferencePlanner` - 1. `dotnet new web -n GraphQL` - 1. `dotnet sln add GraphQL` -1. Add a new folder `Data` where we want to place all our database related code. - 1. `mkdir GraphQL/Data` +# Creating a new GraphQL server project + +- [Registering the DB Context Service](#registering-the-db-context-service) +- [Configuring EF Migrations](#configuring-ef-migrations) + - [Option 1 - Command line](#option-1---command-line) + - [Option 2 - Visual Studio: Package Manager Console](#option-2---visual-studio-package-manager-console) +- [Adding GraphQL](#adding-graphql) +- [Adding a Query](#adding-a-query) +- [Adding a Mutation](#adding-a-mutation) +- [Summary](#summary) + +1. To begin, create a new project for our GraphQL server: + - `dotnet new sln --name ConferencePlanner` + - `dotnet new web --name GraphQL` + - `dotnet sln add GraphQL` + +1. Update the `launchSettings.json` file in the `Properties` directory as follows: + - Remove the `iisSettings` and the `IIS Express` profile. + - Change `launchBrowser` to `false` in the `http` and `https` profiles. + - Change the HTTP port to `5000` and the HTTPS port to `7000`, in the `applicationUrl` properties. + +1. Add the following to the `` in `GraphQL.csproj`: + + ```xml + ConferencePlanner.GraphQL + ``` + +1. Add a new directory named `Data` where we will place all of our database-related code: + + ```shell + mkdir GraphQL/Data + ``` + 1. Add a new file `Speaker.cs` in the `Data` directory using the following code: ```csharp using System.ComponentModel.DataAnnotations; - namespace ConferencePlanner.GraphQL.Data + namespace ConferencePlanner.GraphQL.Data; + + public sealed class Speaker { - public class Speaker - { - public int Id { get; set; } + public int Id { get; init; } - [Required] - [StringLength(200)] - public string Name { get; set; } + [StringLength(200)] + public required string Name { get; init; } - [StringLength(4000)] - public string Bio { get; set; } + [StringLength(4000)] + public string? Bio { get; init; } - [StringLength(1000)] - public virtual string WebSite { get; set; } - } + [StringLength(1000)] + public string? Website { get; init; } } ``` -1. Add a reference to the NuGet package package `Microsoft.EntityFrameworkCore.Sqlite` version `5.0.0` and also Microsoft.EntityFrameworkCore.Sqlite.Design. - 1. `dotnet add GraphQL package Microsoft.EntityFrameworkCore.Sqlite --version 5.0.0` - 2. ` dotnet add GraphQL package Microsoft.EntityFrameworkCore.Sqlite.Design` -1. Next we'll create a new Entity Framework DbContext. Create a new `ApplicationDbContext` class in the `Data` folder using the following code: +1. Add a reference to the following NuGet packages: + - `Microsoft.EntityFrameworkCore.Relational` version `9.0.1`. + - `dotnet add GraphQL package Microsoft.EntityFrameworkCore.Relational --version 9.0.1` + - `Npgsql.EntityFrameworkCore.PostgreSQL` version `9.0.3`. + - `dotnet add GraphQL package Npgsql.EntityFrameworkCore.PostgreSQL --version 9.0.3` + +1. Next, create a new Entity Framework Core DbContext class named `ApplicationDbContext` in the `Data` directory, using the following code: ```csharp using Microsoft.EntityFrameworkCore; - namespace ConferencePlanner.GraphQL.Data - { - public class ApplicationDbContext : DbContext - { - public ApplicationDbContext(DbContextOptions options) - : base(options) - { - } + namespace ConferencePlanner.GraphQL.Data; - public DbSet Speakers { get; set; } - } + public sealed class ApplicationDbContext(DbContextOptions options) + : DbContext(options) + { + public DbSet Speakers { get; init; } } ``` -## Register the DB Context Service +1. Create a `docker-compose.yml` file at the root of the solution, with the following contents, for running a PostgreSQL server: + + ```yaml + name: graphql-workshop + + services: + graphql-workshop-postgres: + container_name: graphql-workshop-postgres + image: postgres:17.2 + environment: + POSTGRES_USER: graphql_workshop + POSTGRES_PASSWORD: secret + POSTGRES_DB: graphql_workshop + networks: [graphql-workshop] + ports: ["5432:5432"] + volumes: + - type: volume + source: postgres-data + target: /var/lib/postgresql/data + + networks: + graphql-workshop: + name: graphql-workshop + + volumes: + postgres-data: + ``` -1. Add the following code to the top of the `ConfigureServices()` method in `Startup.cs`: +## Registering the DB Context Service - ```csharp - ``` +Replace the code in `Program.cs` with the following: - > This code registers the `ApplicationDbContext` service so it can be injected into resolvers. +```csharp +using GraphQL.Data; +using Microsoft.EntityFrameworkCore; -## Configuring EF Migrations +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddDbContext( + options => options.UseNpgsql("Host=127.0.0.1;Username=graphql_workshop;Password=secret")); -1. Add a reference to the NuGet package `Microsoft.EntityFrameworkCore.Tools` version `5.0.0`. - 1. `dotnet add GraphQL package Microsoft.EntityFrameworkCore.Tools --version 5.0.0` +var app = builder.Build(); -### Option 1 - Visual Studio: Package Manager Console +app.Run(); +``` -1. In Visual Studio, select the Tools -> NuGet Package Manager -> Package Manager Console +> Line 7 registers the `ApplicationDbContext` service so that it can be injected into resolvers. -1. Run the following commands in the Package Manager Console +## Configuring EF Migrations - ```console - Add-Migration Initial - Update-Database - ``` +1. Add a reference to the NuGet package `Microsoft.EntityFrameworkCore.Design` version `9.0.1`: + - `dotnet add GraphQL package Microsoft.EntityFrameworkCore.Design --version 9.0.1` -### Option 2 - Command line +1. Start the database server using Docker Compose: -1. Install the EntityFramework global tool `dotnet-ef` using the following command: + ```shell + docker compose up --detach + ``` - ```console - dotnet new tool-manifest - dotnet tool install dotnet-ef --version 5.0.0 --local - ``` +### Option 1 - Command line -2. Open a command prompt and navigate to the project directory. (The directory containing the solution `ConferencePlanner.sln` file). +1. Install the Entity Framework Core tool (`dotnet-ef`) using the following commands: -3. Run the following commands in the command prompt: + ```shell + dotnet new tool-manifest + dotnet tool install dotnet-ef --local --version 9.0.1 + ``` - ```console - dotnet build GraphQL +1. Run the following commands in the command prompt: + + ```shell + dotnet build dotnet ef migrations add Initial --project GraphQL dotnet ef database update --project GraphQL ``` +### Option 2 - Visual Studio: Package Manager Console + +1. In Visual Studio, select `Tools -> NuGet Package Manager -> Package Manager Console`. + +1. Run the following commands in the Package Manager Console: + + ```console + Add-Migration Initial + Update-Database + ``` + Commands Explained | Command | Description | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `dotnet ef migrations add Initial` / `Add-Migration Initial` | generates code to create the initial database schema based on the model specified in 'ApplicationDbContext.cs'. `Initial` is the name of the migration. | -| `dotnet ef database update` / `Update-Database` | creates the database | +| `dotnet ef migrations add Initial` / `Add-Migration Initial` | Generates code to create the initial database schema based on the model specified in `ApplicationDbContext.cs`. `Initial` is the name of the migration. | +| `dotnet ef database update` / `Update-Database` | Creates the database. | -> If your database ever gets in a bad state and you'd like to reset things, you can use `dotnet ef database drop` followed by `dotnet ef database update` to remove your database and run all migrations again. +> If your database ever gets in a bad state and you'd like to reset things, you can use `dotnet ef database drop --project GraphQL` followed by `dotnet ef database update --project GraphQL` to remove your database and run all migrations again. ## Adding GraphQL -1. Add a reference to the NuGet package package `HotChocolate.AspNetCore` version `11.0.0`. - 1. `dotnet add GraphQL package HotChocolate.AspNetCore --version 11.0.0` -1. Next we'll create our query root type (`Query.cs`) and add a resolver that fetches all of our speakers. +1. Add a reference to the following NuGet packages: + - `HotChocolate.AspNetCore` version `15.0.3`. + - `dotnet add GraphQL package HotChocolate.AspNetCore --version 15.0.3` + - `HotChocolate.AspNetCore.CommandLine` version `15.0.3` + - `dotnet add GraphQL package HotChocolate.AspNetCore.CommandLine --version 15.0.3` + - `HotChocolate.Types.Analyzers` version `15.0.3` + - `dotnet add GraphQL package HotChocolate.Types.Analyzers --version 15.0.3` - ```csharp - using System.Linq; - using HotChocolate; - using ConferencePlanner.GraphQL.Data; +1. Set up GraphQL by adding the following code below `AddDbContext` in `Program.cs`: - namespace ConferencePlanner.GraphQL - { - public class Query - { - public IQueryable GetSpeakers([Service] ApplicationDbContext context) => - context.Speakers; - } - } + ```csharp + .AddGraphQLServer(); ``` -1. Before we can do anything with our query root type we need to setup GraphQL and register our query root type. Add the following code below `AddDbContext` in the `ConfigureServices()` method in `Startup.cs`: + The above code adds a GraphQL server configuration to the dependency injection container. + +1. Next we need to configure the GraphQL middleware so that the server knows how to execute GraphQL requests. For this, add the following code below `var app` in `Program.cs`: ```csharp - services - .AddGraphQLServer() - .AddQueryType(); + app.MapGraphQL(); ``` - > The above code registers a GraphQL schema with our dependency injection and with that registers our `Query` type. + Also, replace `app.Run();` with `await app.RunWithGraphQLCommandsAsync(args);`. -1. Next we need to configure our GraphQL middleware so that the server knows how to execute GraphQL requests. For this replace `app.UseEndpoints...` with the following code in the method `Configure(IApplicationBuilder app, IWebHostEnvironment env)` in the `Startup.cs` + Your `Program.cs` should now look like the following: ```csharp - app.UseEndpoints(endpoints => - { - endpoints.MapGraphQL(); - }); + using ConferencePlanner.GraphQL.Data; + using Microsoft.EntityFrameworkCore; + + var builder = WebApplication.CreateBuilder(args); + + builder.Services + .AddDbContext( + options => options.UseNpgsql("Host=127.0.0.1;Username=graphql_workshop;Password=secret")) + .AddGraphQLServer() + .AddGraphQLTypes(); + + var app = builder.Build(); + + app.MapGraphQL(); + + await app.RunWithGraphQLCommandsAsync(args); ``` - > Your Startup.cs should now look like the following: +## Adding a Query + +1. Create a `Queries` class (`Queries.cs`) and add a query that fetches all of our speakers: ```csharp - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - using ConferencePlanner.GraphQL; using ConferencePlanner.GraphQL.Data; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; - namespace GraphQL + namespace ConferencePlanner.GraphQL; + + public static class Queries { - public class Startup + [Query] + public static async Task> GetSpeakersAsync( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - // 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.AddDbContext( - options => options.UseSqlite("Data Source=conferences.db")); - - services - .AddGraphQLServer() - .AddQueryType(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGraphQL(); - }); - } + return await dbContext.Speakers.AsNoTracking().ToListAsync(cancellationToken); } } ``` -1. Start the server. - 1. `dotnet run --project GraphQL` +1. Register our types by adding the following code below `AddGraphQLServer` in `Program.cs`: + + ```csharp + .AddGraphQLTypes(); + ``` + + This registers all types in the assembly using a source generator (`HotChocolate.Types.Analyzers`). + + > Note: The name of the `AddGraphQLTypes` method is based on the assembly name by default, but can be changed using the `[Module]` assembly attribute. + +1. Start the server: + - `dotnet run --project GraphQL` - ![Start GraphQL server](images/1-start-server.png) + ![Start GraphQL server](images/1-start-server.webp) -1. Start [Banana Cake Pop](https://chillicream.com/docs/bananacakepop) or use it built-in your browser at [http://localhost:5000/graphql/](http://localhost:5000/graphql/) and connect to our server (usually at [http://localhost:5000/graphql](http://localhost:5000/graphql)). - **Note**: `
/graphql/` might **not** show mutations, make sure you use `
/graphql` (without trailing slash). +1. Start [Nitro](https://get-nitro.chillicream.com) or use it built into your browser at [http://localhost:5000/graphql/](http://localhost:5000/graphql/), and connect to our server. - ![Connect to GraphQL server with Banana Cake Pop](images/2-bcp-connect-to-server.png) + ![Connect to GraphQL server with Nitro](images/2-bcp-connect-to-server.webp) -1. Click in the schema explorer and click on the `speakers` field in order to check the return type of the `speakers` field. - **Note**: You might have to reload the schema, you can do so by clicking the refresh-button in the upper-right corner. +1. Click the `Browse Schema` button, switch to the `Column View`, and navigate to `Query -> speakers` to view the return type of the `speakers` field. - ![Explore GraphQL schema with Banana Cake Pop](images/3-bcp-schema-explorer.png) + ![Explore GraphQL schema with Nitro](images/3-bcp-browse-schema.webp) -## Adding Mutations +## Adding a Mutation -So, far we have added the Query root type to our schema, which allows us to query speakers. However, at this point, there is no way to add or modify any data. In this section, we are going to add the root Mutation type to add new speakers to our database. +So far we have added a query to our schema, which allows us to query speakers. However, at this point, there is no way to add or modify any data. In this section, we are going to add a mutation for adding new speakers to our database. -> For mutations we are using the [relay mutation pattern](https://relay.dev/docs/v10.1.3/graphql-server-specification/#mutations) which is commonly used in GraphQL. +> For mutations we are using the Relay mutation pattern. +> +> Relay uses a common pattern for mutations, where there are root fields on the mutation type with a single argument, `input`. +> +> By convention, mutations are named as verbs, their inputs are the name with `Input` appended at the end, and they return an object that is the name with `Payload` appended. -A mutation consists of three components, the **input**, the **payload** and the **mutation** itself. In our case we want to create a mutation called `addSpeaker`, by convention, mutations are named as verbs, their inputs are the name with "Input" appended at the end, and they return an object that is the name with "Payload" appended. +A mutation consists of three components, the **input**, the **payload**, and the **mutation** itself. In our case we want to create a mutation named `addSpeaker`. So, for our `addSpeaker` mutation, we create two types: `AddSpeakerInput` and `AddSpeakerPayload`. -1. Add a file `AddSpeakerInput.cs` to your project with the following code: +1. Add a file named `AddSpeakerInput.cs` to your project with the following code: ```csharp - namespace ConferencePlanner.GraphQL - { - public record AddSpeakerInput( - string Name, - string Bio, - string WebSite); - } - ``` + namespace ConferencePlanner.GraphQL; - > The input and output (payload) both contain a client mutation identifier used to reconcile requests and responses in some client frameworks. + public sealed record AddSpeakerInput( + string Name, + string? Bio, + string? Website); + ``` -1. Next we add our `AddSpeakerPayload` which represents the output of our GraphQL mutation by adding the following code: +1. Next, we add our `AddSpeakerPayload`, which represents the output of our GraphQL mutation by adding the following code to `AddSpeakerPayload.cs`: ```csharp using ConferencePlanner.GraphQL.Data; - namespace ConferencePlanner.GraphQL - { - public class AddSpeakerPayload - { - public AddSpeakerPayload(Speaker speaker) - { - Speaker = speaker; - } + namespace ConferencePlanner.GraphQL; - public Speaker Speaker { get; } - } + public sealed class AddSpeakerPayload(Speaker speaker) + { + public Speaker Speaker { get; } = speaker; } ``` -1. Now lets add the actual mutation type with our `addSpeaker` mutation in it. +1. Now let's add the actual mutation type with our `addSpeaker` mutation in it, to a file named `Mutations.cs`: ```csharp - using System.Threading.Tasks; using ConferencePlanner.GraphQL.Data; - using HotChocolate; - namespace ConferencePlanner.GraphQL + namespace ConferencePlanner.GraphQL; + + public static class Mutations { - public class Mutation + [Mutation] + public static async Task AddSpeakerAsync( + AddSpeakerInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) { - public async Task AddSpeakerAsync( - AddSpeakerInput input, - [Service] ApplicationDbContext context) + var speaker = new Speaker { - var speaker = new Speaker - { - Name = input.Name, - Bio = input.Bio, - WebSite = input.WebSite - }; - - context.Speakers.Add(speaker); - await context.SaveChangesAsync(); - - return new AddSpeakerPayload(speaker); - } + Name = input.Name, + Bio = input.Bio, + Website = input.Website + }; + + dbContext.Speakers.Add(speaker); + + await dbContext.SaveChangesAsync(cancellationToken); + + return new AddSpeakerPayload(speaker); } } ``` -1. Last but not least you need to add the new `Mutation` type to your schema: +1. Start the server again in order to validate if it is working properly: - ```csharp - services - .AddGraphQLServer() - .AddQueryType() - .AddMutationType(); + ```shell + dotnet run --project GraphQL ``` -1. Start the server again in order to validate if it is working properly. - 1. `dotnet run --project GraphQL` +1. Explore the changes to the schema with Nitro. There should now be a mutation type and the `addSpeaker` mutation. -1. Explore with Banana Cake Pop the changes schema to the schema. There should now be a mutation type and the `addSpeaker` mutation. - ![GraphQL type explorer](images/4-bcp-schema-explorer-mutation.png) + ![GraphQL type explorer](images/4-bcp-browse-schema-mutation.webp) -1. Next add a speaker by writing a GraphQL mutation. +1. Next, in the `Operation` tab `Request` pane, add a speaker by writing a GraphQL mutation: ```graphql mutation AddSpeaker { - addSpeaker(input: { - name: "Speaker Name" - bio: "Speaker Bio" - webSite: "http://speaker.website" }) { + addSpeaker( + input: { + name: "Speaker name" + bio: "Speaker bio" + website: "https://speaker.website" + } + ) { speaker { id } @@ -323,9 +359,9 @@ So, for our `addSpeaker` mutation, we create two types: `AddSpeakerInput` and `A } ``` - ![Add speaker](images/5-bcp-mutation-add-addspeaker.png) + ![Add speaker](images/5-bcp-mutation-add-speaker.webp) -1. Query the names of all the speakers in the database. +1. Query the names of all the speakers in the database: ```graphql query GetSpeakerNames { @@ -335,12 +371,10 @@ So, for our `addSpeaker` mutation, we create two types: `AddSpeakerInput` and `A } ``` - ![Query speaker names](images/6-bcp-query-get-speakers.png) + ![Query speaker names](images/6-bcp-query-get-speakers.webp) ## Summary -In this first session, you have learned how you can create a simple GraphQL project on top of ASP.NET Core. -You have leveraged Entity Framework to create your models and save those to the database. -Together, ASP.NET Core, Entity Framework, and Hot Chocolate let you build a simple GraphQL server quickly. +In this first session, you have learned how you can create a simple GraphQL project on top of ASP.NET Core. You have leveraged Entity Framework to create your models and save those to the database. Together, ASP.NET Core, Entity Framework, and Hot Chocolate let you build a simple GraphQL server quickly. -[**Session #2 - Controlling nullability >>**](2-controlling-nullability.md) +[**Session #2 - Understanding DataLoader >>**](2-understanding-data-loader.md) diff --git a/docs/2-controlling-nullability.md b/docs/2-controlling-nullability.md deleted file mode 100644 index 496cad2..0000000 --- a/docs/2-controlling-nullability.md +++ /dev/null @@ -1,101 +0,0 @@ -- [Controlling nullability](#controlling-nullability) - - [Configure Nullability](#configure-nullability) - - [Summary](#summary) - -# Controlling nullability - -## Configure Nullability - -The GraphQL type system distinguishes between nullable and non-nullable types. This helps the consumer of the API by providing guarantees when a field value can be trusted to never be null or when an input is not allowed to be null. The ability to rely on such type information simplifies the code of the null since we do not have to write a ton of null checks for things that will never be null. - -1. Open the project file of your GraphQL server project `GraphQL.csproj` and add the following property: - - ```xml - enable - ``` - - You project file now look like the following: - - ```xml - - - - net5.0 - ConferencePlanner.GraphQL - enable - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - ``` - -1. Build your project. - 1. `dotnet build` - - > The compiler will now output a lot of warnings about properties that are now not nullable that are likely to be null. In GraphQL types are by default nullable whereas in C# types are per default not nullable. - -1. The compiler is complaining that the `ApplicationDBContext` property `Speakers` might be null when the type is created. The Entity Framework is setting this field dynamically so the compiler can not see that this field will actually be set. So, in order to fix this lets tell the compiler not to worry about it by assigning `default!` to it: - - ```csharp - public DbSet Speakers { get; set; } = default!; - ``` - -1. Next update the speaker model by marking all the reference types as nullable. - - > The schema still will infer nullability correct since the schema understands the data annotations. - - ```csharp - using System.ComponentModel.DataAnnotations; - - namespace ConferencePlanner.GraphQL.Data - { - public class Speaker - { - public int Id { get; set; } - - [Required] - [StringLength(200)] - public string? Name { get; set; } - - [StringLength(4000)] - public string? Bio { get; set; } - - [StringLength(1000)] - public virtual string? WebSite { get; set; } - } - } - ``` - -1. Now update the input type by marking nullable fields. - - ```csharp - namespace ConferencePlanner.GraphQL - { - public record AddSpeakerInput( - string Name, - string? Bio, - string? WebSite); - } - ``` - - > The payload type can stay for now as it is. - -1. Start your server again and verify the nullability changes in your schema explorer. - - 1. `dotnet run --project GraphQL` - - ![Query speaker names](images/39-bcp-verify-nullability.png) - -## Summary - -In this session, we have further discovered the GraphQL type system, by understanding how nullability works in GraphQL and how Hot Chocolate infers nullability from .NET types. - -[**<< Session #1 - Building a basic GraphQL server API**](1-creating-a-graphql-server-project.md) | [**Session #3 - Understanding GraphQL query execution and DataLoader >>**](3-understanding-dataLoader.md) \ No newline at end of file diff --git a/docs/2-understanding-data-loader.md b/docs/2-understanding-data-loader.md new file mode 100644 index 0000000..443def9 --- /dev/null +++ b/docs/2-understanding-data-loader.md @@ -0,0 +1,465 @@ +# Understanding DataLoader + +- [Adding the remaining data models](#adding-the-remaining-data-models) +- [Adding a DataLoader](#adding-a-dataloader) +- [Type extensions](#type-extensions) +- [Summary](#summary) + +## Adding the remaining data models + +In order to expand our GraphQL server model further we have several more data models to add, and unfortunately it's a little mechanical. You can copy the following classes manually, or open the [session 2 solution](/code/session-2). + +1. Create an `Attendee.cs` class in the `Data` directory with the following code: + + ```csharp + using System.ComponentModel.DataAnnotations; + + namespace ConferencePlanner.GraphQL.Data; + + public sealed class Attendee + { + public int Id { get; init; } + + [StringLength(200)] + public required string FirstName { get; init; } + + [StringLength(200)] + public required string LastName { get; init; } + + [StringLength(200)] + public required string Username { get; init; } + + [StringLength(256)] + public string? EmailAddress { get; init; } + + public ICollection SessionsAttendees { get; init; } = + new List(); + } + ``` + +1. Create a `Session.cs` class with the following code: + + ```csharp + using System.ComponentModel.DataAnnotations; + + namespace ConferencePlanner.GraphQL.Data; + + public sealed class Session + { + public int Id { get; init; } + + [StringLength(200)] + public required string Title { get; init; } + + [StringLength(4000)] + public string? Abstract { get; init; } + + public DateTimeOffset? StartTime { get; set; } + + public DateTimeOffset? EndTime { get; set; } + + // Bonus points to those who can figure out why this is written this way. + public TimeSpan Duration => + EndTime?.Subtract(StartTime ?? EndTime ?? DateTimeOffset.MinValue) ?? + TimeSpan.Zero; + + public int? TrackId { get; set; } + + public ICollection SessionSpeakers { get; init; } = + new List(); + + public ICollection SessionAttendees { get; init; } = + new List(); + + public Track? Track { get; init; } + } + ``` + +1. Create a `Track.cs` class with the following code: + + ```csharp + using System.ComponentModel.DataAnnotations; + + namespace ConferencePlanner.GraphQL.Data; + + public sealed class Track + { + public int Id { get; init; } + + [StringLength(200)] + public required string Name { get; set; } + + public ICollection Sessions { get; init; } = + new List(); + } + ``` + +1. Create a `SessionAttendee.cs` class with the following code: + + ```csharp + namespace ConferencePlanner.GraphQL.Data; + + public sealed class SessionAttendee + { + public int SessionId { get; init; } + + public Session Session { get; init; } = null!; + + public int AttendeeId { get; init; } + + public Attendee Attendee { get; init; } = null!; + } + ``` + +1. Create a `SessionSpeaker.cs` class with the following code: + + ```csharp + namespace ConferencePlanner.GraphQL.Data; + + public sealed class SessionSpeaker + { + public int SessionId { get; init; } + + public Session Session { get; init; } = null!; + + public int SpeakerId { get; init; } + + public Speaker Speaker { get; init; } = null!; + } + ``` + +1. Next, modify the `Speaker` class and add the following property to it: + + ```csharp + public ICollection SessionSpeakers { get; init; } = + new List(); + ``` + + The class should now look like the following: + + ```csharp + using System.ComponentModel.DataAnnotations; + + namespace ConferencePlanner.GraphQL.Data; + + public sealed class Speaker + { + public int Id { get; init; } + + [StringLength(200)] + public required string Name { get; init; } + + [StringLength(4000)] + public string? Bio { get; init; } + + [StringLength(1000)] + public string? Website { get; init; } + + public ICollection SessionSpeakers { get; init; } = + new List(); + } + ``` + +1. Last but not least, update the `ApplicationDbContext` with the following code: + + ```csharp + using Microsoft.EntityFrameworkCore; + + namespace ConferencePlanner.GraphQL.Data; + + public sealed class ApplicationDbContext(DbContextOptions options) + : DbContext(options) + { + public DbSet Attendees { get; init; } + + public DbSet Sessions { get; init; } + + public DbSet Speakers { get; init; } + + public DbSet Tracks { get; init; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .HasIndex(a => a.Username) + .IsUnique(); + + // Many-to-many: Session <-> Attendee + modelBuilder + .Entity() + .HasKey(sa => new { sa.SessionId, sa.AttendeeId }); + + // Many-to-many: Speaker <-> Session + modelBuilder + .Entity() + .HasKey(ss => new { ss.SessionId, ss.SpeakerId }); + } + } + ``` + +Now that we have all of our models in we need to create another migration and update our database. + +1. First, validate your project by building it: + + ```shell + dotnet build GraphQL + ``` + +1. Next, generate a new migration for the database: + + ```shell + dotnet ef migrations add Refactoring --project GraphQL + ``` + +1. Lastly, update the database with the new migration: + + ```shell + dotnet ef database update --project GraphQL + ``` + +After having everything in let's have a look at our schema and see if something changed. + +1. Start your server: + + ```shell + dotnet run --project GraphQL + ``` + +1. Open Nitro. + +1. Head over to the `Schema Reference` tab and have a look at the speaker. + - **Note**: You might have to reload the schema. You can do so by clicking the `Reload Schema` button in the upper right corner. + + ![Connect to GraphQL server with Nitro](images/10-bcp-schema-updated.webp) + +## Adding a DataLoader + +The idea of a [DataLoader](https://github.com/graphql/dataloader) is to batch multiple requests into one call to the database. + +While we could write DataLoaders as individual classes, there is also a source generator to remove some of the boilerplate code. + +1. Add a new class named `DataLoaders` with the following code: + + ```csharp + using ConferencePlanner.GraphQL.Data; + using GreenDonut.Data; + using Microsoft.EntityFrameworkCore; + + namespace ConferencePlanner.GraphQL; + + public static class DataLoaders + { + [DataLoader] + public static async Task> SpeakerByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Speakers + .AsNoTracking() + .Where(s => ids.Contains(s.Id)) + .Select(s => s.Id, selector) + .ToDictionaryAsync(s => s.Id, cancellationToken); + } + } + ``` + + The source generator will generate DataLoader classes for methods with the `[DataLoader]` attribute. + +1. Add a new method named `GetSpeakerAsync` to your `Queries.cs` file: + + ```csharp + [Query] + public static async Task GetSpeakerAsync( + int id, + ISpeakerByIdDataLoader speakerById, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakerById.Select(selection).LoadAsync(id, cancellationToken); + } + ``` + + The `Queries.cs` file should now look like the following: + + ```csharp + using ConferencePlanner.GraphQL.Data; + using GreenDonut.Data; + using HotChocolate.Execution.Processing; + using Microsoft.EntityFrameworkCore; + + namespace ConferencePlanner.GraphQL; + + public static class Queries + { + [Query] + public static async Task> GetSpeakersAsync( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + return await dbContext.Speakers.AsNoTracking().ToListAsync(cancellationToken); + } + + [Query] + public static async Task GetSpeakerAsync( + int id, + ISpeakerByIdDataLoader speakerById, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakerById.Select(selection).LoadAsync(id, cancellationToken); + } + } + ``` + +1. Let's have a look at the new schema with Nitro. For this, start your server and refresh Nitro: + + ```shell + dotnet run --project GraphQL + ``` + + ![Connect to GraphQL server with Nitro](images/11-bcp-schema-updated.webp) + +1. Now, test if the new field works correctly: + + ```graphql + query GetSpecificSpeakerById { + a: speaker(id: 1) { + name + } + b: speaker(id: 1) { + name + } + } + ``` + + ![Connect to GraphQL server with Nitro](images/12-bcp-speaker-query.webp) + + If you look at the console output, you'll see that only a single SQL query is executed, instead of one for each speaker, and that only the `Name` and `Id` columns are selected, instead of all columns in the table, by using DataLoader projections: + + ```console + info: Microsoft.EntityFrameworkCore.Database.Command[20101] + Executed DbCommand (1ms) [Parameters=[@__ids_0='?' (DbType = Object)], CommandType='Text', CommandTimeout='30'] + SELECT s."Name", s."Id" + FROM "Speakers" AS s + WHERE s."Id" = ANY (@__ids_0) + ``` + +## Type extensions + +At the moment, we are purely inferring the schema from our C# classes. In some cases where we have everything under control, this might be a good thing, and everything is okay. + +But if we, for instance, have some parts of the API not under our control and want to change the GraphQL schema representation of these APIs, type extensions can help. With Hot Chocolate, we can mix in these type extensions where we need them. + +In our specific case, we want to make the GraphQL API nicer and remove the relationship objects like `SessionSpeaker`. + +1. First let's add a new DataLoader in order to efficiently fetch sessions by speaker ID. Add the following method to the `DataLoaders` class: + + ```csharp + [DataLoader] + public static async Task> SessionsBySpeakerIdAsync( + IReadOnlyList speakerIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Speakers + .AsNoTracking() + .Where(s => speakerIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionSpeakers.Select(ss => ss.Session), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } + ``` + +1. Create a new directory named `Types`: + + ```shell + mkdir GraphQL/Types + ``` + +1. Create a new class named `SpeakerType` in the `Types` directory, with the following code: + + ```csharp + using ConferencePlanner.GraphQL.Data; + using GreenDonut.Data; + using HotChocolate.Execution.Processing; + + namespace ConferencePlanner.GraphQL.Types; + + [ObjectType] + public static partial class SpeakerType + { + [BindMember(nameof(Speaker.SessionSpeakers))] + public static async Task> GetSessionsAsync( + [Parent] Speaker speaker, + ISessionsBySpeakerIdDataLoader sessionsBySpeakerId, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsBySpeakerId + .Select(selection) + .LoadRequiredAsync(speaker.Id, cancellationToken); + } + } + ``` + + In this type extension, we replace the existing `sessionSpeakers` field (property `SessionSpeakers`), with a new field named `sessions` (method `GetSessionsAsync`), using the `[BindMember]` attribute. The new field exposes the sessions associated with the speaker. + +1. The new GraphQL representation of our speaker type is now: + + ```GraphQL + type Speaker { + sessions: [Session!]! + id: Int! + name: String! + bio: String + website: String + } + ``` + +1. Next, create another class named `SessionType` in the `Types` directory, with the following code: + + ```csharp + using ConferencePlanner.GraphQL.Data; + + namespace ConferencePlanner.GraphQL.Types; + + [ObjectType] + public static partial class SessionType + { + public static TimeSpan Duration([Parent("StartTime EndTime")] Session session) + => session.Duration; + } + ``` + + In this type extension, we add a resolver for the `duration` field of the `Session` type, so that we can specify its requirements using the `[Parent]` attribute. In this case, `StartTime` and `EndTime` are required in order to compute the `Duration` in the `Session` class. + +1. Start your GraphQL server again: + + ```shell + dotnet run --project GraphQL + ``` + +1. Go back to Nitro, refresh the schema, and execute the following query: + + ```graphql + query GetSpeakerWithSessions { + speaker(id: 1) { + name + sessions { + title + duration + } + } + } + ``` + + Since we do not have any data for sessions yet the server will return an empty list of sessions. Still, our server works already and we will soon be able to add more data. + +## Summary + +In this session, we've added DataLoaders to our GraphQL API and learned what DataLoaders are. We've also looked at a new way to extend our GraphQL types with type extensions, which lets us change the shape of types that we don't want to annotate with GraphQL attributes. + +[**<< Session #1 - Creating a new GraphQL server project**](1-creating-a-graphql-server-project.md) | [**Session #3 - GraphQL schema design approaches >>**](3-schema-design.md) diff --git a/docs/3-schema-design.md b/docs/3-schema-design.md new file mode 100644 index 0000000..27e1fb0 --- /dev/null +++ b/docs/3-schema-design.md @@ -0,0 +1,891 @@ +# GraphQL schema design approaches + +- [Reorganizing types](#reorganizing-types) + - [Reorganizing query types](#reorganizing-query-types) + - [Reorganizing mutation types](#reorganizing-mutation-types) + - [Reorganizing object types](#reorganizing-object-types) + - [Reorganizing DataLoaders](#reorganizing-dataloaders) +- [Enabling Mutation Conventions](#enabling-mutation-conventions) +- [Enabling Global Object Identification](#enabling-global-object-identification) +- [Building out the schema](#building-out-the-schema) + - [Thinking beyond CRUD](#thinking-beyond-crud) + - [Offering plural versions of fields and being precise about field names](#offering-plural-versions-of-fields-and-being-precise-about-field-names) +- [Summary](#summary) + +In GraphQL, most APIs are designed in [Relay](https://relay.dev/) style. Relay is Facebook's GraphQL client for React and represents Facebook's opinionated view on GraphQL. The GraphQL community adopted the [Relay server specification](https://relay.dev/docs/guides/graphql-server-specification/) since it provides a battle-tested way of exposing GraphQL at massive scale. + +The two core assumptions that Relay makes about a GraphQL server are that it provides: + +1. A mechanism for refetching an object. +1. A description of how to page through connections. + +## Reorganizing types + +First, we will restructure our GraphQL server so that it will better scale once we add more types. With Hot Chocolate, we can split types into multiple classes, which is especially useful with root types. Splitting our root types allows us to organize our queries, mutations, and subscriptions by topic rather than having all of them in one massive class. Moreover, in tests, we can load only the parts of a query, mutation, or subscription type that we need. + +### Reorganizing query types + +1. Create a new directory named `Speakers`: + + ```shell + mkdir GraphQL/Speakers + ``` + +1. Move the `Queries.cs` file to the `Speakers` directory and rename it to `SpeakerQueries.cs`: + + ```shell + mv GraphQL/Queries.cs GraphQL/Speakers/SpeakerQueries.cs + ``` + +1. Now, update the namespace and class name, annotate the renamed class with the `[QueryType]` attribute, and remove the `[Query]` attribute from the methods. The class should look like this now: + + ```csharp + using ConferencePlanner.GraphQL.Data; + using GreenDonut.Data; + using HotChocolate.Execution.Processing; + using Microsoft.EntityFrameworkCore; + + namespace ConferencePlanner.GraphQL.Speakers; + + [QueryType] + public static class SpeakerQueries + { + public static async Task> GetSpeakersAsync( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + return await dbContext.Speakers.AsNoTracking().ToListAsync(cancellationToken); + } + + public static async Task GetSpeakerAsync( + int id, + ISpeakerByIdDataLoader speakerById, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakerById.Select(selection).LoadAsync(id, cancellationToken); + } + } + ``` + +### Reorganizing mutation types + +1. Move the `Mutations.cs` file to the `Speakers` directory and rename it to `SpeakerMutations.cs`: + + ```shell + mv GraphQL/Mutations.cs GraphQL/Speakers/SpeakerMutations.cs + ``` + +1. Now, update the namespace and class name, annotate the renamed class with the `[MutationType]` attribute, and remove the `[Mutation]` attribute from the method. The class should look like this now: + + ```csharp + using ConferencePlanner.GraphQL.Data; + + namespace ConferencePlanner.GraphQL.Speakers; + + [MutationType] + public static class SpeakerMutations + { + public static async Task AddSpeakerAsync( + AddSpeakerInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var speaker = new Speaker + { + Name = input.Name, + Bio = input.Bio, + Website = input.Website + }; + + dbContext.Speakers.Add(speaker); + + await dbContext.SaveChangesAsync(cancellationToken); + + return speaker; + } + } + ``` + +1. Move the `AddSpeakerInput.cs` file into the `Speakers` directory, and update the namespace: + + ```shell + mv GraphQL/AddSpeakerInput.cs GraphQL/Speakers/AddSpeakerInput.cs + ``` + + ```diff + - namespace ConferencePlanner.GraphQL; + + namespace ConferencePlanner.GraphQL.Speakers; + ``` + +### Reorganizing object types + +1. Create a new directory named `Sessions`: + + ```shell + mkdir GraphQL/Sessions + ``` + +1. Move the `SessionType.cs` file from the `Types` directory to the `Sessions` directory, and update the namespace: + + ```shell + mv GraphQL/Types/SessionType.cs GraphQL/Sessions/SessionType.cs + ``` + + ```diff + - namespace ConferencePlanner.GraphQL.Types; + + namespace ConferencePlanner.GraphQL.Sessions; + ``` + +1. Move the `SpeakerType.cs` file from the `Types` directory to the `Speakers` directory, and update the namespace: + + ```shell + mv GraphQL/Types/SpeakerType.cs GraphQL/Speakers/SpeakerType.cs + ``` + + ```diff + - namespace ConferencePlanner.GraphQL.Types; + + namespace ConferencePlanner.GraphQL.Speakers; + ``` + +1. Delete the empty `Types` directory: + + ```shell + rm --dir GraphQL/Types + ``` + +### Reorganizing DataLoaders + +1. Move the `DataLoaders.cs` file to the `Speakers` directory and rename it to `SpeakerDataLoaders.cs`: + + ```shell + mv GraphQL/DataLoaders.cs GraphQL/Speakers/SpeakerDataLoaders.cs + ``` + +1. Now, update the namespace and class name: + + ```diff + - namespace ConferencePlanner.GraphQL; + + namespace ConferencePlanner.GraphQL.Speakers; + ``` + + ```diff + - public static class DataLoaders + + public static class SpeakerDataLoaders + ``` + +## Enabling Mutation Conventions + +Hot Chocolate has built-in conventions for mutations to minimize boilerplate code. Instead of manually creating payload types, Hot Chocolate can generate these types for us automatically. + +1. Enable mutation conventions by adding the following line in `Program.cs`: + + ```diff + .AddGraphQLServer() + + .AddMutationConventions() + ``` + +1. Update the `AddSpeakerAsync` method in `SpeakerMutations.cs` to return the `Speaker` directly, instead of the `AddSpeakerPayload`: + + ```diff + - public static async Task AddSpeakerAsync( + + public static async Task AddSpeakerAsync( + ``` + + ```diff + - return new AddSpeakerPayload(speaker); + + return speaker; + ``` + +1. Delete the `AddSpeakerPayload.cs` file, as this file is no longer needed: + + ```shell + rm GraphQL/AddSpeakerPayload.cs + ``` + +## Enabling Global Object Identification + +The first thing that we have to do here is to enable [Global Object Identification](https://chillicream.com/docs/hotchocolate/v14/defining-a-schema/relay/#global-object-identification) on the schema. After that, we'll focus on the first Relay server specification called [Object Identification](https://relay.dev/docs/guides/graphql-server-specification/#object-identification). + +1. Enable Global Object Identification for the schema in `Program.cs`: + + ```diff + .AddGraphQLServer() + + .AddGlobalObjectIdentification() + .AddMutationConventions() + ``` + +1. Update the `GetSpeakerAsync` method in `SpeakerQueries.cs` by adding the `[NodeResolver]` attribute: + + ```diff + + [NodeResolver] + public static async Task GetSpeakerAsync( + ``` + + The `NodeResolver` attribute marks the node resolver for a Relay node type (in this case, the `Speaker` type). It will also set the GraphQL type of the `id` parameter to `ID`. + +1. Start the GraphQL server: + + ```shell + dotnet run --project GraphQL + ``` + +1. Head to Nitro and refresh the schema. + + ![Explore Relay Node Field](images/13-bcp-node-field.webp) + +## Building out the schema + +This step will add more DataLoaders and schema types. While this will be a bit mechanical, it will form the basis for our ventures into proper GraphQL schema design. + +We'll start by adding the rest of the DataLoaders that we'll need. Then we'll add type extensions for `Attendee` and `Track`, and update the `SessionType`. Once we have all of this in, we'll start diving into some schema design rules and how to apply them. + +1. Create a new directory named `Attendees`: + + ```shell + mkdir GraphQL/Attendees + ``` + +1. Add a new static class named `AttendeeDataLoaders` to the `Attendees` directory, with `AttendeeByIdAsync` and `SessionsByAttendeeIdAsync` DataLoaders: + + ```csharp + using ConferencePlanner.GraphQL.Data; + using GreenDonut.Data; + using Microsoft.EntityFrameworkCore; + + namespace ConferencePlanner.GraphQL.Attendees; + + public static class AttendeeDataLoaders + { + [DataLoader] + public static async Task> AttendeeByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Attendees + .AsNoTracking() + .Where(a => ids.Contains(a.Id)) + .Select(a => a.Id, selector) + .ToDictionaryAsync(a => a.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SessionsByAttendeeIdAsync( + IReadOnlyList attendeeIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Attendees + .AsNoTracking() + .Where(a => attendeeIds.Contains(a.Id)) + .Select(a => a.Id, a => a.SessionsAttendees.Select(sa => sa.Session), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } + } + ``` + +1. Add a new static class named `SessionDataLoaders` to the `Sessions` directory, with `SessionByIdAsync`, `SpeakersBySessionIdAsync`, and `AttendeesBySessionIdAsync` DataLoaders: + + ```csharp + using ConferencePlanner.GraphQL.Data; + using GreenDonut.Data; + using Microsoft.EntityFrameworkCore; + + namespace ConferencePlanner.GraphQL.Sessions; + + public static class SessionDataLoaders + { + [DataLoader] + public static async Task> SessionByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => ids.Contains(s.Id)) + .Select(s => s.Id, selector) + .ToDictionaryAsync(s => s.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SpeakersBySessionIdAsync( + IReadOnlyList sessionIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => sessionIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionSpeakers.Select(ss => ss.Speaker), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } + + [DataLoader] + public static async Task> AttendeesBySessionIdAsync( + IReadOnlyList sessionIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => sessionIds.Contains(s.Id)) + .Select(s => s.Id, s => s.SessionAttendees.Select(sa => sa.Attendee), selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } + } + ``` + +1. Add a new static class named `TrackDataLoaders` to the `Tracks` directory, with `TrackByIdAsync` and `SessionsByTrackIdAsync` DataLoaders: + + ```csharp + using ConferencePlanner.GraphQL.Data; + using GreenDonut.Data; + using Microsoft.EntityFrameworkCore; + + namespace ConferencePlanner.GraphQL.Tracks; + + public static class TrackDataLoaders + { + [DataLoader] + public static async Task> TrackByIdAsync( + IReadOnlyList ids, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Tracks + .AsNoTracking() + .Where(t => ids.Contains(t.Id)) + .Select(t => t.Id, selector) + .ToDictionaryAsync(t => t.Id, cancellationToken); + } + + [DataLoader] + public static async Task> SessionsByTrackIdAsync( + IReadOnlyList trackIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + CancellationToken cancellationToken) + { + return await dbContext.Tracks + .AsNoTracking() + .Where(t => trackIds.Contains(t.Id)) + .Select(t => t.Id, t => t.Sessions, selector) + .ToDictionaryAsync(r => r.Key, r => r.Value.ToArray(), cancellationToken); + } + } + ``` + +1. Now, add the missing type classes, `AttendeeType` and `TrackType`: + + `GraphQL/Attendees/AttendeeType.cs` + + ```csharp + using ConferencePlanner.GraphQL.Data; + using GreenDonut.Data; + using HotChocolate.Execution.Processing; + + namespace ConferencePlanner.GraphQL.Attendees; + + [ObjectType] + public static partial class AttendeeType + { + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .ImplementsNode() + .IdField(a => a.Id) + .ResolveNode( + async (ctx, id) + => await ctx.DataLoader() + .LoadAsync(id, ctx.RequestAborted)); + } + + [BindMember(nameof(Attendee.SessionsAttendees))] + public static async Task> GetSessionsAsync( + [Parent] Attendee attendee, + ISessionsByAttendeeIdDataLoader sessionsByAttendeeId, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsByAttendeeId + .Select(selection) + .LoadRequiredAsync(attendee.Id, cancellationToken); + } + } + ``` + + Note that since we're not exposing an `attendeeById` query where we could have applied the `[NodeResolver]` attribute, we instead define the node resolver using the descriptor: + + - `ImplementsNode` marks the type as implementing the `Node` interface. + - `IdField` specifies the ID member of the node type. + - `ResolveNode` specifies a delegate to resolve the node from its ID. + + `GraphQL/Tracks/TrackType.cs` + + ```csharp + using ConferencePlanner.GraphQL.Data; + using GreenDonut.Data; + using HotChocolate.Execution.Processing; + + namespace ConferencePlanner.GraphQL.Tracks; + + [ObjectType] + public static partial class TrackType + { + public static async Task> GetSessionsAsync( + [Parent] Track track, + ISessionsByTrackIdDataLoader sessionsByTrackId, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsByTrackId + .Select(selection) + .LoadRequiredAsync(track.Id, cancellationToken); + } + } + ``` + +1. Finally, update the `SessionType` (`GraphQL/Sessions/SessionType.cs`) by adding 3 additional resolvers, and configuring the `TrackId` as a Relay ID. + + ```csharp + using ConferencePlanner.GraphQL.Data; + using ConferencePlanner.GraphQL.Tracks; + using GreenDonut.Data; + using HotChocolate.Execution.Processing; + + namespace ConferencePlanner.GraphQL.Sessions; + + [ObjectType] + public static partial class SessionType + { + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field(s => s.TrackId) + .ID(); + } + + public static TimeSpan Duration([Parent("StartTime EndTime")] Session session) + => session.Duration; + + [BindMember(nameof(Session.SessionSpeakers))] + public static async Task> GetSpeakersAsync( + [Parent] Session session, + ISpeakersBySessionIdDataLoader speakersBySessionId, + ISelection selection, + CancellationToken cancellationToken) + { + return await speakersBySessionId + .Select(selection) + .LoadRequiredAsync(session.Id, cancellationToken); + } + + [BindMember(nameof(Session.SessionAttendees))] + public static async Task> GetAttendeesAsync( + [Parent(nameof(Session.Id))] Session session, + IAttendeesBySessionIdDataLoader attendeesBySessionId, + ISelection selection, + CancellationToken cancellationToken) + { + return await attendeesBySessionId + .Select(selection) + .LoadRequiredAsync(session.Id, cancellationToken); + } + + public static async Task GetTrackAsync( + [Parent(nameof(Session.TrackId))] Session session, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + if (session.TrackId is null) + { + return null; + } + + return await trackById + .Select(selection) + .LoadAsync(session.TrackId.Value, cancellationToken); + } + } + ``` + +Great, we now have our base schema and are ready to dive into some schema design topics. Although GraphQL has a single root query type, a single root mutation type, and a single root subscription type, Hot Chocolate allows splitting the root types into multiple classes, which will enable us to organize our schema around topics rather than divide it along its root types. + +### Thinking beyond CRUD + +GraphQL represents a much better way to expose APIs over HTTP. GraphQL wants us to think beyond standard [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) APIs. By using action- or behavior-specific fields and mutations, we can create a more human-readable API that helps clients to use it. + +In this chapter, we'll design our mutation API by really thinking about the use cases of our conference API. We don't just want to expose our database model to the user; we want to create an understandable and easy-to-use API driven by use cases rather than the raw data structures. + +First, we'll focus on the sessions. The session is the primary data model we are interacting with. People want to look up sessions, schedule sessions, search for sessions, or even file new sessions. + +Conferences typically first ask for papers; after some time, they will accept some of the proposed talks. After more time, they will build from these sessions the schedule. Often the program is divided into tracks. A talk will also often be moved around until the conference starts, but even at this point, schedule changes might happen. + +This reflection on our subject at hand leads us to two mutations that we need. First, we need to be able to add new sessions; then, we need to be able to schedule sessions on a specific track and time slot. + +1. Add a new file named `SessionExceptions.cs` in the `Sessions` directory, with the following code: + + ```csharp + namespace ConferencePlanner.GraphQL.Sessions; + + public sealed class EndTimeInvalidException() : Exception("EndTime must be after StartTime."); + + public sealed class NoSpeakerException() : Exception("No speaker assigned."); + + public sealed class SessionNotFoundException() : Exception("Session not found."); + + public sealed class TitleEmptyException() : Exception("The title cannot be empty."); + ``` + +1. Add a new record named `AddSessionInput` in the `Sessions` directory, with the following code: + + ```csharp + using ConferencePlanner.GraphQL.Data; + + namespace ConferencePlanner.GraphQL.Sessions; + + public sealed record AddSessionInput( + string Title, + string? Abstract, + [property: ID] IReadOnlyList SpeakerIds); + ``` + +1. Next, add a new static class named `SessionMutations` in the `Sessions` directory, with the following code: + + ```csharp + using ConferencePlanner.GraphQL.Data; + + namespace ConferencePlanner.GraphQL.Sessions; + + [MutationType] + public static class SessionMutations + { + [Error] + [Error] + public static async Task AddSessionAsync( + AddSessionInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(input.Title)) + { + throw new TitleEmptyException(); + } + + if (input.SpeakerIds.Count == 0) + { + throw new NoSpeakerException(); + } + + var session = new Session + { + Title = input.Title, + Abstract = input.Abstract + }; + + foreach (var speakerId in input.SpeakerIds) + { + session.SessionSpeakers.Add(new SessionSpeaker + { + SpeakerId = speakerId + }); + } + + dbContext.Sessions.Add(session); + + await dbContext.SaveChangesAsync(cancellationToken); + + return session; + } + } + ``` + + The `Error` attribute registers a middleware that will catch all exceptions of type `TError` on mutations and queries. By annotating the attribute the response type of the annotated resolver will be automatically extended. + + > Our `addSession` mutation will only let you specify the title, the abstract, and the speakers. + +1. Next, add a `ScheduleSessionInput` record to our `Sessions` directory with the following code: + + ```csharp + using ConferencePlanner.GraphQL.Data; + + namespace ConferencePlanner.GraphQL.Sessions; + + public sealed record ScheduleSessionInput( + [property: ID] int SessionId, + [property: ID] int TrackId, + DateTimeOffset StartTime, + DateTimeOffset EndTime); + ``` + +1. Now, add the following `scheduleSession` mutation to the `SessionMutations` class: + + ```csharp + [Error] + [Error] + public static async Task ScheduleSessionAsync( + ScheduleSessionInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + if (input.EndTime < input.StartTime) + { + throw new EndTimeInvalidException(); + } + + var session = await dbContext.Sessions.FindAsync([input.SessionId], cancellationToken); + + if (session is null) + { + throw new SessionNotFoundException(); + } + + session.TrackId = input.TrackId; + session.StartTime = input.StartTime; + session.EndTime = input.EndTime; + + await dbContext.SaveChangesAsync(cancellationToken); + + return session; + } + ``` + + While we are now able to add sessions and then schedule them, we still need some mutations to create and rename tracks. + +1. Create a new directory named `Tracks`: + + ```shell + mkdir GraphQL/Tracks + ``` + +1. Add a new file named `TrackExceptions.cs` in the `Tracks` directory, with the following code: + + ```csharp + namespace ConferencePlanner.GraphQL.Tracks; + + public sealed class TrackNotFoundException() : Exception("Track not found."); + ``` + +1. Add a record named `AddTrackInput` to the `Tracks` directory with the following code: + + ```csharp + namespace ConferencePlanner.GraphQL.Tracks; + + public sealed record AddTrackInput(string Name); + ``` + +1. Now that you have the input file in, create a new static class named `TrackMutations` with the following code: + + ```csharp + using ConferencePlanner.GraphQL.Data; + + namespace ConferencePlanner.GraphQL.Tracks; + + [MutationType] + public static class TrackMutations + { + public static async Task AddTrackAsync( + AddTrackInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var track = new Track { Name = input.Name }; + + dbContext.Tracks.Add(track); + + await dbContext.SaveChangesAsync(cancellationToken); + + return track; + } + } + ``` + +1. Next, we need to get our `renameTrack` mutation in. For this create a new record named `RenameTrackInput` and place it in the `Tracks` directory: + + ```csharp + using ConferencePlanner.GraphQL.Data; + + namespace ConferencePlanner.GraphQL.Tracks; + + public sealed record RenameTrackInput([property: ID] int Id, string Name); + ``` + +1. Lastly, we'll add the `renameTrack` mutation to our `TrackMutations` class: + + ```csharp + [Error] + public static async Task RenameTrackAsync( + RenameTrackInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var track = await dbContext.Tracks.FindAsync([input.Id], cancellationToken); + + if (track is null) + { + throw new TrackNotFoundException(); + } + + track.Name = input.Name; + + await dbContext.SaveChangesAsync(cancellationToken); + + return track; + } + ``` + +1. Start your GraphQL server and verify that your mutations work by adding some sessions, creating tracks, and scheduling the sessions to the tracks: + + ```shell + dotnet run --project GraphQL + ``` + +> The DateTime format in GraphQL is specified by [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339) and looks like the following: `1985-04-12T23:20:50.52Z`. More about the GraphQL `DateTime` scalar can be found here: + +### Offering plural versions of fields and being precise about field names + +With GraphQL, we want to think about efficiency a lot. For instance, we offer mutations with one `input` argument so that clients can assign this argument from one variable without needing to deconstruct. Almost every little aspect in GraphQL is done so that you can request data more efficiently. That is why we should also design our schema in such a way that we allow users of our API to fetch multiple entities in one go. + +Sure, we can technically do that already: + +```graphql +{ + speaker1: speaker(id: 1) { + name + } + speaker2: speaker(id: 2) { + name + } +} +``` + +But with plural versions, we can specify a variable of IDs and pass that into a query without modifying the query text itself. By doing that, we can use static queries on our client and also let the query engine of the GraphQL server optimize this static query for execution. Further, we can write a resolver that is optimized to fetch data in one go. Offering plural fields allows for more flexibility and better performance. + +The second aspect here is to be more specific with our field names. The name `speaker` is quite unspecific, and we'll already start to see a problem with this once we introduce a plural version of it named `speakers`, since we already have a field named `speakers` that is the list of speakers. A good choice in `GraphQL` would be to name the fields `speakerById` and the second one, `speakersById`. + +In this section, we'll optimize our `Query` type by bringing in more fields to query our API. Also we'll restructure our query type to offer plural versions of our fields that fetch by ID. + +1. Head over to your `SpeakerQueries` class and update the `GetSpeakerAsync` method to be named `GetSpeakerByIdAsync`: + + ```diff + [NodeResolver] + - public static async Task GetSpeakerAsync( + + public static async Task GetSpeakerByIdAsync( + ``` + +1. Next, introduce a new `GetSpeakersByIdAsync` method as our plural version: + + ```csharp + public static async Task> GetSpeakersByIdAsync( + [ID] int[] ids, + ISpeakerByIdDataLoader speakerById, + CancellationToken cancellationToken) + { + return await speakerById.LoadRequiredAsync(ids, cancellationToken); + } + ``` + + > Note that the DataLoader can also fetch multiple entities for us. + +1. Add a new static class named `SessionQueries` to the `Sessions` directory with the following code: + + ```csharp + using ConferencePlanner.GraphQL.Data; + using GreenDonut.Data; + using HotChocolate.Execution.Processing; + using Microsoft.EntityFrameworkCore; + + namespace ConferencePlanner.GraphQL.Sessions; + + [QueryType] + public static class SessionQueries + { + public static async Task> GetSessionsAsync( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + return await dbContext.Sessions.AsNoTracking().ToListAsync(cancellationToken); + } + + [NodeResolver] + public static async Task GetSessionByIdAsync( + int id, + ISessionByIdDataLoader sessionById, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionById.Select(selection).LoadAsync(id, cancellationToken); + } + + public static async Task> GetSessionsByIdAsync( + [ID] int[] ids, + ISessionByIdDataLoader sessionById, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionById.Select(selection).LoadRequiredAsync(ids, cancellationToken); + } + } + ``` + +1. Next, add a new static class named `TrackQueries` to the `Tracks` directory with the following code: + + ```csharp + using ConferencePlanner.GraphQL.Data; + using GreenDonut.Data; + using HotChocolate.Execution.Processing; + using Microsoft.EntityFrameworkCore; + + namespace ConferencePlanner.GraphQL.Tracks; + + [QueryType] + public static class TrackQueries + { + public static async Task> GetTracksAsync( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + return await dbContext.Tracks.AsNoTracking().ToListAsync(cancellationToken); + } + + [NodeResolver] + public static async Task GetTrackByIdAsync( + int id, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + return await trackById.Select(selection).LoadAsync(id, cancellationToken); + } + + public static async Task> GetTracksByIdAsync( + [ID] int[] ids, + ITrackByIdDataLoader trackById, + ISelection selection, + CancellationToken cancellationToken) + { + return await trackById.Select(selection).LoadRequiredAsync(ids, cancellationToken); + } + } + ``` + +1. Start your GraphQL server and verify with Nitro that you can use the new queries: + + ```shell + dotnet run --project GraphQL + ``` + +## Summary + +We've covered quite a lot in this section. We've learned that GraphQL is designed for efficiency and that many of the schema design concepts are designed around this core principle of GraphQL. We looked at how mutations should be structured, and that we should aim to design GraphQL schemas not around a database schema, but instead around our business model. With GraphQL we have such strong capabilities to express our business model that we should look beyond simple CRUD. Expressively design your schema so that each mutation conveys very clearly what it does. Allow your consumers to fetch single entities or multiple entities without forcing them to use aliases. + +[**<< Session #2 - Understanding DataLoader**](2-understanding-data-loader.md) | [**Session #4 - Understanding middleware >>**](4-understanding-middleware.md) diff --git a/docs/3-understanding-dataLoader.md b/docs/3-understanding-dataLoader.md deleted file mode 100644 index 2d375eb..0000000 --- a/docs/3-understanding-dataLoader.md +++ /dev/null @@ -1,734 +0,0 @@ -- [Understanding GraphQL query execution and DataLoader](#understanding-graphql-query-execution-and-dataloader) - - [Configure field scoped services](#configure-field-scoped-services) - - [Adding the remaining data models](#adding-the-remaining-data-models) - - [Adding DataLoader](#adding-dataloader) - - [Fluent type configurations](#fluent-type-configurations) - - [Summary](#summary) - -# Understanding GraphQL query execution and DataLoader - -## Configure field scoped services - -The GraphQL execution engine will always try to execute fields in parallel in order to optimize data-fetching and reduce wait time. Entity Framework will have a problem with that since a `DBContext` is not thread-safe. Let us first create the issue and run into this problem before fixing it. - -1. Start your GraphQL Server. - 1. `dotnet run --project Graphql` - -1. Start Banana Cake Pop and run the following query: - - ```graphql - query GetSpeakerNamesInParallel { - a: speakers { - name - bio - } - b: speakers { - name - bio - } - c: speakers { - name - bio - } - } - ``` - - ![Connect to GraphQL server with Banana Cake Pop](images/8-bcp-dbcontext-error.png) - - We ran the field to fetch the speaker three times in parallel, which used the same `DBContext` and lead to the exception by the `DBContext`. - - We have the option either set the execution engine to execute serially, which is terrible for performance or to use `DBContext` pooling in combination with field scoped services. - - Using `DBContext` pooling allows us to issue a `DBContext` instance for each field needing one. But instead of creating a `DBContext` instance for every field and throwing it away after using it, we are renting so fields and requests can reuse it. - -1. Head over to the `Startup.cs` and replace `services.AddDbContext` with `services.AddPooledDbContextFactory`. - - old: - `services.AddDbContext(options => options.UseSqlite("Data Source=conferences.db"));` - - new: - `services.AddPooledDbContextFactory(options => options.UseSqlite("Data Source=conferences.db"));` - - > By default the `DBContext` pool will keep 128 `DBContext` instances in its pool. - -1. Create a new folder called `Extensions` - 1. `mkdir GraphQL/Extensions` - -1. Create a new file located in `Extensions` called `ObjectFieldDescriptorExtensions.cs` with the following code: - - ```csharp - using Microsoft.EntityFrameworkCore; - using Microsoft.Extensions.DependencyInjection; - using HotChocolate.Types; - - namespace ConferencePlanner.GraphQL - { - public static class ObjectFieldDescriptorExtensions - { - public static IObjectFieldDescriptor UseDbContext( - this IObjectFieldDescriptor descriptor) - where TDbContext : DbContext - { - return descriptor.UseScopedService( - create: s => s.GetRequiredService>().CreateDbContext()/*, - disposeAsync: (s, c) => c.DisposeAsync()*/); // this would lead to an Error with Disposing the DBContext - } - } - } - ``` - - > The `UseDbContext` will create a new middleware that handles scoping for a field. - > The `create` part will rent from the pool a `DBContext`, the `dispose` - > part will return it after the middleware is finished. - > All of this is handled transparently through the new `IDbContextFactory` introduced - > with .NET 5. - -1. Create another file located in `Extensions` called `UseApplicationDbContextAttribute.cs` with the following code: - - ```csharp - using System.Reflection; - using ConferencePlanner.GraphQL.Data; - using HotChocolate.Types; - using HotChocolate.Types.Descriptors; - - namespace ConferencePlanner.GraphQL - { - public class UseApplicationDbContextAttribute : ObjectFieldDescriptorAttribute - { - public override void OnConfigure( - IDescriptorContext context, - IObjectFieldDescriptor descriptor, - MemberInfo member) - { - descriptor.UseDbContext(); - } - } - } - ``` - - > The above code creates a so-called descriptor-attribute and allows us to wrap GraphQL - > configuration code into attributes that you can apply to .NET type system members. - -1. Next, head over to the `Query.cs` and change it like the following: - - ```csharp - using System.Collections.Generic; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using HotChocolate; - - - namespace ConferencePlanner.GraphQL - { - public class Query - { - [UseApplicationDbContext] - public Task> GetSpeakers([ScopedService] ApplicationDbContext context) => - context.Speakers.ToListAsync(); - } - } - ``` - - > By annotating `UseApplicationDbContext` we are essentially applying a Middleware to the field resolver pipeline. We will have a more in-depth look into field middleware later on. - - > **Important**: Note, that we no longer are returning the `IQueryable` but are executing the `IQueryable` by using `ToListAsync`. We will explain why later in the middleware and filter session. - -1. Now head over to the `Mutation.cs` and do the same there: - - ```csharp - using System.Threading.Tasks; - using ConferencePlanner.GraphQL.Data; - using HotChocolate; - - namespace ConferencePlanner.GraphQL - { - public class Mutation - { - [UseApplicationDbContext] - public async Task AddSpeakerAsync( - AddSpeakerInput input, - [ScopedService] ApplicationDbContext context) - { - var speaker = new Speaker - { - Name = input.Name, - Bio = input.Bio, - WebSite = input.WebSite - }; - - context.Speakers.Add(speaker); - await context.SaveChangesAsync(); - - return new AddSpeakerPayload(speaker); - } - } - } - ``` - -1. Start your GraphQL Server again. - 1. `dotnet run --project Graphql` - -1. Start Banana Cake Pop again and run the following query again: - - ```graphql - query GetSpeakerNamesInParallel { - a: speakers { - name - bio - } - b: speakers { - name - bio - } - c: speakers { - name - bio - } - } - ``` - - ![Connect to GraphQL server with Banana Cake Pop](images/9-bcp-dbcontext-works-inparallel.png) - - This time our query works like expected. - -## Adding the remaining data models - -In order to expand our GraphQL server model further we've got several more data models to add, and unfortunately it's a little mechanical. You can copy the following classes manually, or open the [session 3 solution](/code/session-3). - -1. Create an `Attendee.cs` class in the `Data` directory with the following code: - - ```csharp - using System.Collections.Generic; - using System.ComponentModel.DataAnnotations; - - namespace ConferencePlanner.GraphQL.Data - { - public class Attendee - { - public int Id { get; set; } - - [Required] - [StringLength(200)] - public string? FirstName { get; set; } - - [Required] - [StringLength(200)] - public string? LastName { get; set; } - - [Required] - [StringLength(200)] - public string? UserName { get; set; } - - [StringLength(256)] - public string? EmailAddress { get; set; } - - public ICollection SessionsAttendees { get; set; } = - new List(); - } - } - ``` - -1. Create a `Session.cs` class with the following code: - - ```csharp - using System; - using System.Collections.Generic; - using System.ComponentModel.DataAnnotations; - - namespace ConferencePlanner.GraphQL.Data - { - public class Session - { - public int Id { get; set; } - - [Required] - [StringLength(200)] - public string? Title { get; set; } - - [StringLength(4000)] - public string? Abstract { get; set; } - - public DateTimeOffset? StartTime { get; set; } - - public DateTimeOffset? EndTime { get; set; } - - // Bonus points to those who can figure out why this is written this way - public TimeSpan Duration => - EndTime?.Subtract(StartTime ?? EndTime ?? DateTimeOffset.MinValue) ?? - TimeSpan.Zero; - - public int? TrackId { get; set; } - - public ICollection SessionSpeakers { get; set; } = - new List(); - - public ICollection SessionAttendees { get; set; } = - new List(); - - public Track? Track { get; set; } - } - } - ``` - -1. Create a new `Track.cs` class with the following code: - - ```csharp - using System.Collections.Generic; - using System.ComponentModel.DataAnnotations; - - namespace ConferencePlanner.GraphQL.Data - { - public class Track - { - public int Id { get; set; } - - [Required] - [StringLength(200)] - public string? Name { get; set; } - - public ICollection Sessions { get; set; } = - new List(); - } - } - ``` - -1. Create a `SessionAttendee.cs` class with the following code: - - ```csharp - namespace ConferencePlanner.GraphQL.Data - { - public class SessionAttendee - { - public int SessionId { get; set; } - - public Session? Session { get; set; } - - public int AttendeeId { get; set; } - - public Attendee? Attendee { get; set; } - } - } - ``` - -1. Create a `SessionSpeaker.cs` class with the following code: - - ```csharp - namespace ConferencePlanner.GraphQL.Data - { - public class SessionSpeaker - { - public int SessionId { get; set; } - - public Session? Session { get; set; } - - public int SpeakerId { get; set; } - - public Speaker? Speaker { get; set; } - } - } - ``` - -1. Next, modify the `Speaker` class and add the following property to it: - - ```csharp - public ICollection SessionSpeakers { get; set; } = - new List(); - ``` - - The class should now look like the following: - - ```csharp - using System.Collections.Generic; - using System.ComponentModel.DataAnnotations; - - namespace ConferencePlanner.GraphQL.Data - { - public class Speaker - { - public int Id { get; set; } - - [Required] - [StringLength(200)] - public string? Name { get; set; } - - [StringLength(4000)] - public string? Bio { get; set; } - - [StringLength(1000)] - public string? WebSite { get; set; } - - public ICollection SessionSpeakers { get; set; } = - new List(); - } - } - ``` - -1. Last but not least, update the `ApplicationDbContext` with the following code: - - ```csharp - using Microsoft.EntityFrameworkCore; - - namespace ConferencePlanner.GraphQL.Data - { - public class ApplicationDbContext : DbContext - { - public ApplicationDbContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasIndex(a => a.UserName) - .IsUnique(); - - // Many-to-many: Session <-> Attendee - modelBuilder - .Entity() - .HasKey(ca => new { ca.SessionId, ca.AttendeeId }); - - // Many-to-many: Speaker <-> Session - modelBuilder - .Entity() - .HasKey(ss => new { ss.SessionId, ss.SpeakerId }); - } - - public DbSet Sessions { get; set; } = default!; - - public DbSet Tracks { get; set; } = default!; - - public DbSet Speakers { get; set; } = default!; - - public DbSet Attendees { get; set; } = default!; - } - } - ``` - -Now, that we have all of our models in we need to create another migration and update our database. - -1. First, validate your project by building it. - - ```console - dotnet build GraphQL - ``` - -1. Next, generate a new migration for the database. - - ```console - dotnet ef migrations add Refactoring --project GraphQL - ``` - -1. Last, update the database with the new migration. - - ```console - dotnet ef database update --project GraphQL - ``` - -After having everything in let us have a look at our schema and see if something changed. - -1. Start, your server. - - ```console - dotnet run --project GraphQL - ``` - -1. Open Banana Cake Pop and refresh the schema. - -2. Head over to the schema explorer and have a look at the speaker. **Note**: You might have to reload the schema, you can do so by clicking the refresh-button in the upper-right corner. - - ![Connect to GraphQL server with Banana Cake Pop](images/10-bcp-schema-updated.png) - -## Adding DataLoader - -1. Add a new directory `DataLoader` to your project: - - ```console - mkdir GraphQL/DataLoader - ``` - -1. Add a new class called `SpeakerByIdDataLoader` to the `DataLoader` directory with the following code: - - ```csharp - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using GreenDonut; - using HotChocolate.DataLoader; - - namespace ConferencePlanner.GraphQL.DataLoader - { - public class SpeakerByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public SpeakerByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Speakers - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } - } - ``` - -1. Now, register your `DataLoader` with the schema like the following in the `Startup.cs`: - - ```csharp - services - .AddGraphQLServer() - .AddQueryType() - .AddMutationType() - .AddDataLoader(); - ``` - -1. Add a new method `GetSpeakerAsync` to your `Query.cs`. - - ```csharp - public Task GetSpeakerAsync( - int id, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - dataLoader.LoadAsync(id, cancellationToken); - ``` - - The `Query.cs` should now look like the following: - - ```csharp - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.DataLoader; - using HotChocolate; - - namespace ConferencePlanner.GraphQL - { - public class Query - { - [UseApplicationDbContext] - public Task> GetSpeakers([ScopedService] ApplicationDbContext context) => - context.Speakers.ToListAsync(); - - public Task GetSpeakerAsync( - int id, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - dataLoader.LoadAsync(id, cancellationToken); - } - } - ``` - -1. Let us have a look at the new schema with Banana Cake Pop. For this start your server and refresh Banana Cake Pop. - - ```console - dotnet run --project GraphQL - ``` - - ![Connect to GraphQL server with Banana Cake Pop](images/11-bcp-schema-updated.png) - -1. Now try out if the new field works right. - - ```graphql - query GetSpecificSpeakerById { - a: speaker(id: 1) { - name - } - b: speaker(id: 1) { - name - } - } - ``` - - ![Connect to GraphQL server with Banana Cake Pop](images/12-bcp-speaker-query.png) - -## Fluent type configurations - -At this very moment, we are purely inferring the schema from our C# classes. In some cases where we have everything under control, this might be a good thing, and everything is okay. - -But if we, for instance, have some parts of the API not under our control and want to change the GraphQL schema representation of these APIs, fluent type configurations can help. With Hot Chocolate, we can mix in those type configurations where we need them or even go full in and declare our whole schema purely with our fluent type API. - -In our specific case, we want to make the GraphQL API nicer and remove the relationship objects like `SessionSpeaker`. - -1. First let us add a new `DataLoader` for sessions in order to efficiently fetch sessions. Let's create a file `SessionByIdDataLoader.cs` for this with the following code: - - ```csharp - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using GreenDonut; - using HotChocolate.DataLoader; - - namespace ConferencePlanner.GraphQL.DataLoader - { - public class SessionByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public SessionByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Sessions - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } - } - ``` - -1. Register the new `DataLoader` with the schema. - - ```csharp - services - .AddGraphQLServer() - .AddQueryType() - .AddMutationType() - .AddDataLoader() - .AddDataLoader(); - ``` - -1. Create a new directory `Types`. - - ```console - mkdir GraphQL/Types - ``` - -1. Create a new class `SpeakerType` in the directory types with the following code: - - ```csharp - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.DataLoader; - using HotChocolate; - using HotChocolate.Types; - - namespace ConferencePlanner.GraphQL.Types - { - public class SpeakerType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .Field(t => t.SessionSpeakers) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("sessions"); - } - - private class SpeakerResolvers - { - public async Task> GetSessionsAsync( - Speaker speaker, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] sessionIds = await dbContext.Speakers - .Where(s => s.Id == speaker.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SessionId)) - .ToArrayAsync(); - - return await sessionById.LoadAsync(sessionIds, cancellationToken); - } - } - } - } - ``` - - > In the type configuration we are giving `SessionSpeakers` a new name `sessions`. - > Also, we are binding a new resolver to this field which also rewrites the result type. - > The new field `sessions` now returns `[Session]`. - -1. Register the type with the schema builder in the `Startup.cs`: - - ```csharp - services - .AddGraphQLServer() - .AddQueryType() - .AddMutationType() - .AddType() - .AddDataLoader() - .AddDataLoader(); - ``` - - The new GraphQL representation of our speaker type is now: - - ```GraphQL - type Speaker { - sessions: [Sessions] - id: Int! - name: String! - bio: String - website: String - } - ``` - -1. Start your GraphQL server again. - - ```console - dotnet run --project GraphQL - ``` - -1. Go back to Banana Cake Pop, refresh the schema and execute the following query: - - ```graphql - query GetSpeakerWithSessions { - speakers { - name - sessions { - title - } - } - } - ``` - - > Since we do not have any data for sessions yet the server will return an empty list for session. Still, our server works already and we will soon be able to add more data. - -## Summary - -In this session, we have looked a GraphQL query execution and how it impacts how work with Entity Framework. We have used field scopes services in combination with `IDbContextFactory` and DBContext pooling to mitigate the impact of parallel execution with Entity Framework. Further, we have added `DataLoader` to our GraphQL API and learned what DataLoader is. Last but not least we have looked at a new way to describe our GraphQL types with a fluent approach which lets us change the shape of types that we do not want to annotate with GraphQL attributes. - -[**<< Session #2 - Controlling nullability**](2-controlling-nullability.md) | [**Session #4 - GraphQL schema design approaches >>**](4-schema-design.md) diff --git a/docs/4-schema-design.md b/docs/4-schema-design.md deleted file mode 100644 index 93364e3..0000000 --- a/docs/4-schema-design.md +++ /dev/null @@ -1,1352 +0,0 @@ -- [GraphQL schema design approaches](#graphql-schema-design-approaches) - - [Reorganize mutation types](#reorganize-mutation-types) - - [Enable Relay support](#enable-relay-support) - - [Build out the schema](#build-out-the-schema) - - [Think beyond CRUD](#think-beyond-crud) - - [Offer plural versions fields and be precise about field names](#offer-plural-versions-fields-and-be-precise-about-field-names) - - [Summary](#summary) - -# GraphQL schema design approaches - -In GraphQL, most APIs are designed in Relay style. Relay is Facebook's GraphQL client for React and represents Facebook's opinionated view on GraphQL. The GraphQL community adopted the Relay server specifications since it provides a battle-tested way of exposing GraphQL at massive scale. - -Relay makes three core assumptions about a GraphQL server: - -- The server provides a mechanism for refetching an object. -- The server specifies a way of how to page through connections. -- Mutations are structured in a specific way to make them predictable to use. - -## Reorganize mutation types - -First, we will restructure our GraphQL server so that it will better scale once we add more types. With Hot Chocolate, we can split types into multiple classes, which is especially useful with root types. Splitting our root types allows us to organize our queries, mutations, and subscriptions by topic rather then having all of them in one massive class. Moreover, in tests, we can load only the parts of a query-, mutation-, or subscription-type that we need. - -1. Create a new folder `Common`. - - ```console - mkdir GraphQL/Common - ``` - -1. Create a class `Payload.cs` in the `Common` directory with the following code: - - ```csharp - using System.Collections.Generic; - - namespace ConferencePlanner.GraphQL.Common - { - public abstract class Payload - { - protected Payload(IReadOnlyList? errors = null) - { - Errors = errors; - } - - public IReadOnlyList? Errors { get; } - } - } - ``` - -1. Next, we create a new class `UserError` that is also located in the `Common` directory with the following code: - - ```csharp - namespace ConferencePlanner.GraphQL.Common - { - public class UserError - { - public UserError(string message, string code) - { - Message = message; - Code = code; - } - - public string Message { get; } - - public string Code { get; } - } - } - ``` - -Now, that we have some base classes for our mutation let us start to reorganize the mutation type. - -1. Create a new folder `Speakers`. - - ```console - mkdir GraphQL/Speakers - ``` - -1. Move the `Mutation.cs` to the `Speakers` folder and rename it to `SpeakerMutations`. - -1. Now, annotate the renamed class with the `ExtendObjectTypeAttribute.` The class should look like this now: - - ```csharp - using System.Threading; - using System.Threading.Tasks; - using ConferencePlanner.GraphQL.Common; - using ConferencePlanner.GraphQL.Data; - using HotChocolate; - using HotChocolate.Types; - - namespace ConferencePlanner.GraphQL.Speakers - { - [ExtendObjectType("Mutation")] - public class SpeakerMutations - { - [UseApplicationDbContext] - public async Task AddSpeakerAsync( - AddSpeakerInput input, - [ScopedService] ApplicationDbContext context) - { - var speaker = new Speaker - { - Name = input.Name, - Bio = input.Bio, - WebSite = input.WebSite - }; - - context.Speakers.Add(speaker); - await context.SaveChangesAsync(); - - return new AddSpeakerPayload(speaker); - } - } - } - ``` - -1. Move the `AddSpeakerInput.cs` into the `Speakers` directory. - - ```csharp - namespace ConferencePlanner.GraphQL.Speakers - { - public record AddSpeakerInput( - string Name, - string? Bio, - string? WebSite); - } - ``` - -1. Next, create a new class `SpeakerPayloadBase` with the following code: - - ```csharp - using System.Collections.Generic; - using ConferencePlanner.GraphQL.Common; - using ConferencePlanner.GraphQL.Data; - - namespace ConferencePlanner.GraphQL.Speakers - { - public class SpeakerPayloadBase : Payload - { - protected SpeakerPayloadBase(Speaker speaker) - { - Speaker = speaker; - } - - protected SpeakerPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Speaker? Speaker { get; } - } - } - ``` - -1. Now, move the `AddSpeakerPayload` and base it on the new `SpeakerPayloadBase`. The code should now look like the following: - - ```csharp - using System.Collections.Generic; - using ConferencePlanner.GraphQL.Common; - using ConferencePlanner.GraphQL.Data; - - namespace ConferencePlanner.GraphQL.Speakers - { - public class AddSpeakerPayload : SpeakerPayloadBase - { - public AddSpeakerPayload(Speaker speaker) - : base(speaker) - { - } - - public AddSpeakerPayload(IReadOnlyList errors) - : base(errors) - { - } - } - } - ``` - -1. Change the schema configurations so that we can merge the various `Mutation` class that we will have into one. For that replace the schema builder configuration with the following code in the `Startup.cs`: - - ```csharp - services - .AddGraphQLServer() - .AddQueryType() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddType() - .AddDataLoader() - .AddDataLoader(); - ``` - -## Enable Relay support - -Now that we have reorganized our mutations, we will refactor the schema to a proper relay style. The first thing we have to do here is to `EnableRelaySupport` on the schema. After that, we will focus on the first Relay server specification called **Global Object Identification**. - -1. Enable relay support for the schema. - - ```csharp - services - .AddGraphQLServer() - .AddQueryType() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddType() - .EnableRelaySupport() - .AddDataLoader() - .AddDataLoader(); - ``` - -1. Configure the speaker entity to implement the `Node` interface by adding the node configuration to the `SpeakerType`. - - ```csharp - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.DataLoader; - using HotChocolate; - using HotChocolate.Resolvers; - using HotChocolate.Types; - - namespace ConferencePlanner.GraphQL.Types - { - public class SpeakerType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionSpeakers) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("sessions"); - } - - private class SpeakerResolvers - { - public async Task> GetSessionsAsync( - Speaker speaker, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Speakers - .Where(s => s.Id == speaker.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SessionId)) - .ToArrayAsync(); - - return await sessionById.LoadAsync(speakerIds, cancellationToken); - } - } - } - } - ``` - - > The following piece of code marked our `SpeakerType` as implementing the `Node` interface. It also defined that the `id` field that the node interface specifies is implemented by the `Id` on our entity. The internal `Id` is consequently rewritten to a global object identifier that contains the internal id plus the type name. Last but not least we defined a `ResolveNode` that is able to load the entity by `id`. - - > ```csharp - > descriptor - > .ImplementsNode() - > .IdField(t => t.Id) - > .ResolveNode((ctx, id) => ctx.DataLoader() - > .LoadAsync(id, ctx.RequestAborted)); - > ``` - -1. Head over to the `Query.cs` and annotate the `id` argument of `GetSpeaker` with the `ID` attribute. - - ```csharp - public Task GetSpeakerAsync( - [ID(nameof(Speaker))] int id, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - dataLoader.LoadAsync(id, cancellationToken); - ``` - - > Wherever we handle `id` values we need to annotate them with the `ID` attribute in order to tell the execution engine what kind of `ID` this is. We also can do that in the fluent API by using the `ID` descriptor method a field or argument descriptor. - - > ```csharp - > descriptor.Field(t => t.FooId).ID("FOO"); - > ``` - -1. Start the GraphQL server. - - ```console - dotnet run --project GraphQL - ``` - -1. Head to Banana Cake Pop and refresh the schema. - - ![Explore Relay Node Field](images/13-bcp-node-field.png) - -## Build out the schema - -This step will add more DataLoader and schema types, while this will be a bit mechanical, it will form the basis for our ventures into proper GraphQL schema design. - -We will start by adding the rest of the DataLoader that we will need. Then we will add types for `Attendee`, `Track`, and `Session`. Last, we will reorganize our query type so that we can split it as well. Once we have all this in, we will start diving into some schema design rules and how to apply them. - -1. Add missing DataLoader to the `DataLoader` directory. - - `AttendeeByIdDataLoader.cs` - - ```csharp - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using GreenDonut; - using HotChocolate.DataLoader; - - namespace ConferencePlanner.GraphQL.DataLoader - { - public class AttendeeByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public AttendeeByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Attendees - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } - } - ``` - - `TrackByIdDataLoader.cs` - - ```csharp - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using GreenDonut; - using HotChocolate.DataLoader; - - namespace ConferencePlanner.GraphQL.DataLoader - { - public class TrackByIdDataLoader : BatchDataLoader - { - private readonly IDbContextFactory _dbContextFactory; - - public TrackByIdDataLoader( - IBatchScheduler batchScheduler, - IDbContextFactory dbContextFactory) - : base(batchScheduler) - { - _dbContextFactory = dbContextFactory ?? - throw new ArgumentNullException(nameof(dbContextFactory)); - } - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - await using ApplicationDbContext dbContext = - _dbContextFactory.CreateDbContext(); - - return await dbContext.Tracks - .Where(s => keys.Contains(s.Id)) - .ToDictionaryAsync(t => t.Id, cancellationToken); - } - } - } - ``` - -1. Now, add the missing type classes, `AttendeeType`, `TrackType`, and `SessionType` to the `Types` directory. - - `AttendeeType.cs` - - ```csharp - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.DataLoader; - using HotChocolate; - using HotChocolate.Resolvers; - using HotChocolate.Types; - - namespace ConferencePlanner.GraphQL.Types - { - public class AttendeeType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionsAttendees) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("sessions"); - } - - private class AttendeeResolvers - { - public async Task> GetSessionsAsync( - Attendee attendee, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Attendees - .Where(a => a.Id == attendee.Id) - .Include(a => a.SessionsAttendees) - .SelectMany(a => a.SessionsAttendees.Select(t => t.SessionId)) - .ToArrayAsync(); - - return await sessionById.LoadAsync(speakerIds, cancellationToken); - } - } - } - } - ``` - - `SessionType.cs` - - ```csharp - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.DataLoader; - using HotChocolate; - using HotChocolate.Resolvers; - using HotChocolate.Types; - using HotChocolate.Types.Relay; - - namespace ConferencePlanner.GraphQL.Types - { - public class SessionType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.SessionSpeakers) - .ResolveWith(t => t.GetSpeakersAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("speakers"); - - descriptor - .Field(t => t.SessionAttendees) - .ResolveWith(t => t.GetAttendeesAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("attendees"); - - descriptor - .Field(t => t.Track) - .ResolveWith(t => t.GetTrackAsync(default!, default!, default)); - - descriptor - .Field(t => t.TrackId) - .ID(nameof(Track)); - } - - private class SessionResolvers - { - public async Task> GetSpeakersAsync( - Session session, - [ScopedService] ApplicationDbContext dbContext, - SpeakerByIdDataLoader speakerById, - CancellationToken cancellationToken) - { - int[] speakerIds = await dbContext.Sessions - .Where(s => s.Id == session.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SpeakerId)) - .ToArrayAsync(); - - return await speakerById.LoadAsync(speakerIds, cancellationToken); - } - - public async Task> GetAttendeesAsync( - Session session, - [ScopedService] ApplicationDbContext dbContext, - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) - { - int[] attendeeIds = await dbContext.Sessions - .Where(s => s.Id == session.Id) - .Include(session => session.SessionAttendees) - .SelectMany(session => session.SessionAttendees.Select(t => t.AttendeeId)) - .ToArrayAsync(); - - return await attendeeById.LoadAsync(attendeeIds, cancellationToken); - } - - public async Task GetTrackAsync( - Session session, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) - { - if (session.TrackId is null) - { - return null; - } - - return await trackById.LoadAsync(session.TrackId.Value, cancellationToken); - } - } - } - } - ``` - - `TrackType.cs` - - ```csharp - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.DataLoader; - using HotChocolate; - using HotChocolate.Resolvers; - using HotChocolate.Types; - - namespace ConferencePlanner.GraphQL.Types - { - public class TrackType : ObjectType - { - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor - .ImplementsNode() - .IdField(t => t.Id) - .ResolveNode((ctx, id) => - ctx.DataLoader().LoadAsync(id, ctx.RequestAborted)); - - descriptor - .Field(t => t.Sessions) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .Name("sessions"); - } - - private class TrackResolvers - { - public async Task> GetSessionsAsync( - Track track, - [ScopedService] ApplicationDbContext dbContext, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - int[] sessionIds = await dbContext.Sessions - .Where(s => s.Id == track.Id) - .Select(s => s.Id) - .ToArrayAsync(); - - return await sessionById.LoadAsync(sessionIds, cancellationToken); - } - } - } - } - ``` - -1. Move the `Query.cs` to the `Speakers` directory and rename it to `SpeakerQueries.cs`. - -1. Next, add the `[ExtendObjectType("Query")]` on top of our `SpeakerQueries` class. The code should now look like the following. - - ```csharp - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.DataLoader; - using HotChocolate; - using HotChocolate.Types; - using HotChocolate.Types.Relay; - - namespace ConferencePlanner.GraphQL.Speakers - { - [ExtendObjectType("Query")] - public class SpeakerQueries - { - [UseApplicationDbContext] - public Task> GetSpeakers([ScopedService] ApplicationDbContext context) => - context.Speakers.ToListAsync(); - - public Task GetSpeakerAsync( - [ID(nameof(Speaker))]int id, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - dataLoader.LoadAsync(id, cancellationToken); - } - } - ``` - -1. Head over to the `Startup.cs` and lets reconfigure the schema builder like we did with the `Mutation` type. The new schema configuration should look like the following: - - ```csharp - services - .AddGraphQLServer() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddType() - .EnableRelaySupport(); - ``` - -1. Register the `AttendeeType`, `TrackType`, and `SessionType` with the schema builder. - - ```csharp - services - .AddGraphQLServer() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport(); - ``` - -Great, we now have our base schema and are ready to dive into some schema design topics. Although GraphQL has a single root query type, a single root mutation type, and a single root subscription type, Hot Chocolate allows splitting the root types into multiple classes, which will enable us to organize our schema around topics rather than divide it along its root types. - -### Think beyond CRUD - -GraphQL represents a much better way to expose APIs over HTTP. GraphQL wants us to think beyond standard CRUD APIs. By using action or behavior specific fields and mutations, we can create a more human-readable API that helps clients use our API. - -In this chapter, we will design our mutation API by really thinking about the use-cases of our conference API. We do not just want to expose our database model to the user; we want to create an understandable and easy-to-use API driven by use-cases rather than the raw data structures. - -First, we will focus on the sessions. The session is the primary data model we are interacting with. People want to lookup sessions, schedule sessions, search for sessions, or even file new sessions. - -Conferences typically first ask for papers; after some time, they will accept some of the proposed talks. After more time, they will build from these sessions the schedule. Often the program is divided into tracks. A talk will also often be moved around until the conference starts, but even at this point, schedule changes might happen. - -This reflection on our subject at hand leads us to two mutations that we need. First, we need to be able to add new sessions; then, we need to be able to schedule sessions on a specific track and time slot. - -1. Create a new directory called `Sessions` - -```console -mkdir GraphQL/Sessions -``` - -1. Add a new class `SessionPayloadBase` in the `Sessions` directory with the following code: - - > The `SessionPayloadBase` will be the base for all of our session payloads. - - ```csharp - using System.Collections.Generic; - using ConferencePlanner.GraphQL.Common; - using ConferencePlanner.GraphQL.Data; - - namespace ConferencePlanner.GraphQL.Sessions - { - public class SessionPayloadBase : Payload - { - protected SessionPayloadBase(Session session) - { - Session = session; - } - - protected SessionPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Session? Session { get; } - } - } - ``` - -1. Next add a new record `AddSessionInput` in the `Sessions` directory with the following code: - - ```csharp - using System.Collections.Generic; - using ConferencePlanner.GraphQL.Data; - using HotChocolate.Types.Relay; - - namespace ConferencePlanner.GraphQL.Sessions - { - public record AddSessionInput( - string Title, - string? Abstract, - [ID(nameof(Speaker))] - IReadOnlyList SpeakerIds); - } - ``` - -1. Add a new class `AddSessionPayload` in the `Sessions` directory with the following code: - - ```csharp - using ConferencePlanner.GraphQL.Common; - using ConferencePlanner.GraphQL.Data; - - namespace ConferencePlanner.GraphQL.Sessions - { - public class AddSessionPayload : SessionPayloadBase - { - public AddSessionPayload(UserError error) - : base(new[] { error }) - { - } - - public AddSessionPayload(Session session) : base(session) - { - } - - public AddSessionPayload(IReadOnlyList errors) : base(errors) - { - } - } - } - ``` - -1. Now, add a new class `SessionMutations` into the `Sessions` directory with the following code: - - ```csharp - using System.Threading; - using System.Threading.Tasks; - using ConferencePlanner.GraphQL.Common; - using ConferencePlanner.GraphQL.Data; - using HotChocolate; - using HotChocolate.Types; - - namespace ConferencePlanner.GraphQL.Sessions - { - [ExtendObjectType("Mutation")] - public class SessionMutations - { - [UseApplicationDbContext] - public async Task AddSessionAsync( - AddSessionInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(input.Title)) - { - return new AddSessionPayload( - new UserError("The title cannot be empty.", "TITLE_EMPTY")); - } - - if (input.SpeakerIds.Count == 0) - { - return new AddSessionPayload( - new UserError("No speaker assigned.", "NO_SPEAKER")); - } - - var session = new Session - { - Title = input.Title, - Abstract = input.Abstract, - }; - - foreach (int speakerId in input.SpeakerIds) - { - session.SessionSpeakers.Add(new SessionSpeaker - { - SpeakerId = speakerId - }); - } - - context.Sessions.Add(session); - await context.SaveChangesAsync(cancellationToken); - - return new AddSessionPayload(session); - } - } - } - ``` - - > Our `addSession` mutation will only let you specify the title, the abstract and the speakers. - -1. Head back to the `Startup.cs` and add the `SessionMutations` to the schema builder. - - ```csharp - services - .AddGraphQLServer() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport(); - ``` - -1. Next, add the `ScheduleSessionInput` to our `Sessions` directory with the following code: - - ```csharp - using System; - using ConferencePlanner.GraphQL.Data; - using HotChocolate.Types.Relay; - - namespace ConferencePlanner.GraphQL.Sessions - { - public record ScheduleSessionInput( - [ID(nameof(Session))] - int SessionId, - [ID(nameof(Track))] - int TrackId, - DateTimeOffset StartTime, - DateTimeOffset EndTime); - } - ``` - -1. Add the `ScheduleSessionPayload` class to with the following code: - - ```csharp - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Common; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.DataLoader; - using HotChocolate; - - namespace ConferencePlanner.GraphQL.Sessions - { - public class ScheduleSessionPayload : SessionPayloadBase - { - public ScheduleSessionPayload(Session session) - : base(session) - { - } - - public ScheduleSessionPayload(UserError error) - : base(new[] { error }) - { - } - - public async Task GetTrackAsync( - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) - { - if (Session is null) - { - return null; - } - - return await trackById.LoadAsync(Session.Id, cancellationToken); - } - - [UseApplicationDbContext] - public async Task?> GetSpeakersAsync( - [ScopedService] ApplicationDbContext dbContext, - SpeakerByIdDataLoader speakerById, - CancellationToken cancellationToken) - { - if (Session is null) - { - return null; - } - - int[] speakerIds = await dbContext.Sessions - .Where(s => s.Id == Session.Id) - .Include(s => s.SessionSpeakers) - .SelectMany(s => s.SessionSpeakers.Select(t => t.SpeakerId)) - .ToArrayAsync(); - - return await speakerById.LoadAsync(speakerIds, cancellationToken); - } - } - } - ``` - -1. Now, insert the following `scheduleSession` mutation to the `SessionMutations` class: - - ```csharp - [UseApplicationDbContext] - public async Task ScheduleSessionAsync( - ScheduleSessionInput input, - [ScopedService] ApplicationDbContext context) - { - if (input.EndTime < input.StartTime) - { - return new ScheduleSessionPayload( - new UserError("endTime has to be larger than startTime.", "END_TIME_INVALID")); - } - - Session session = await context.Sessions.FindAsync(input.SessionId); - - if (session is null) - { - return new ScheduleSessionPayload( - new UserError("Session not found.", "SESSION_NOT_FOUND")); - } - - session.TrackId = input.TrackId; - session.StartTime = input.StartTime; - session.EndTime = input.EndTime; - - await context.SaveChangesAsync(); - - return new ScheduleSessionPayload(session); - } - ``` - - While we now are able to add sessions and then schedule them, we still need some mutations to create a track or rename a track. - -1. Create a new directory called `Tracks` - - ```console - mkdir GraphQL/Tracks - ``` - -1. Add a class `TrackPayloadBase` to the `Tracks` directory with the following code: - - ```csharp - using System.Collections.Generic; - using ConferencePlanner.GraphQL.Common; - using ConferencePlanner.GraphQL.Data; - - namespace ConferencePlanner.GraphQL.Tracks - { - public class TrackPayloadBase : Payload - { - public TrackPayloadBase(Track track) - { - Track = track; - } - - public TrackPayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Track? Track { get; } - } - } - ``` - -1. Add a class `AddTrackInput` to the `Tracks` directory with the following code: - - ```csharp - namespace ConferencePlanner.GraphQL.Tracks - { - public record AddTrackInput(string Name); - } - ``` - -1. Next, add the `AddTrackPayload` payload class with the following code: - - ```csharp - using System.Collections.Generic; - using ConferencePlanner.GraphQL.Common; - using ConferencePlanner.GraphQL.Data; - - namespace ConferencePlanner.GraphQL.Tracks - { - public class AddTrackPayload : TrackPayloadBase - { - public AddTrackPayload(Track track) - : base(track) - { - } - - public AddTrackPayload(IReadOnlyList errors) - : base(errors) - { - } - } - } - ``` - -1. Now that you have the payload and input files in create a new class `TracksMutations` with the following code: - - ```csharp - using System.Threading; - using System.Threading.Tasks; - using ConferencePlanner.GraphQL.Data; - using HotChocolate; - using HotChocolate.Types; - - namespace ConferencePlanner.GraphQL.Tracks - { - [ExtendObjectType("Mutation")] - public class TrackMutations - { - [UseApplicationDbContext] - public async Task AddTrackAsync( - AddTrackInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - var track = new Track { Name = input.Name }; - context.Tracks.Add(track); - - await context.SaveChangesAsync(cancellationToken); - - return new AddTrackPayload(track); - } - } - } - ``` - -1. Head back to the `Startup.cs` and add the `TrackMutations` to the schema builder. - - ```csharp - services - .AddGraphQLServer() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport() - .AddDataLoader() - .AddDataLoader(); - ``` - -1. Next, we need to get our `renameTrack` mutation in. For this create a new class `RenameTrackInput` and place it in the `Tracks` directory. - - ```csharp - using ConferencePlanner.GraphQL.Data; - using HotChocolate.Types.Relay; - - namespace ConferencePlanner.GraphQL.Tracks - { - public record RenameTrackInput([ID(nameof(Track))] int Id, string Name); - } - ``` - -1. Add a class `RenameTrackPayload` and put it into the `Tracks` directory. - - ```csharp - using System.Collections.Generic; - using ConferencePlanner.GraphQL.Common; - using ConferencePlanner.GraphQL.Data; - - namespace ConferencePlanner.GraphQL.Tracks - { - public class RenameTrackPayload : TrackPayloadBase - { - public RenameTrackPayload(Track track) - : base(track) - { - } - - public RenameTrackPayload(IReadOnlyList errors) - : base(errors) - { - } - } - } - ``` - -1. Last, we will add the `renameTrack` mutation to our `TrackMutations` class. - - ```csharp - [UseApplicationDbContext] - public async Task RenameTrackAsync( - RenameTrackInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - Track track = await context.Tracks.FindAsync(input.Id); - track.Name = input.Name; - - await context.SaveChangesAsync(cancellationToken); - - return new RenameTrackPayload(track); - } - ``` - -1. Start your GraphQL server and verify that your `Mutations` work by adding some sessions, creating tracks and scheduling the sessions to the tracks. - - ```console - dotnet run --project GraphQL - ``` - -> The DateTime format in GraphQL is specified by RFC3399 and looks like the following: `2020-05-24T15:00:00`. More about the GraphQL `DateTime` scalar can be found here: https://www.graphql-scalars.com/date-time/ - -### Offer plural versions fields and be precise about field names - -With GraphQL, we want to think about efficiency a lot. For instance, we offer mutations with one `input` argument so that clients can assign this argument from one variable without needing to deconstruct. Almost every little aspect in GraphQL is done so that you can request data more efficiently. That is why we also should design our schema in such a way that we allow users of our API to fetch multiple entities in one go. - -Sure, we technically can do that already. - -```graphql -{ - speaker1: speaker(id: 1) { - name - } - speaker2: speaker(id: 2) { - name - } -} -``` - -But with plural versions, we can specify a variable of ids and pass that into a query without modifying the query text itself. By doing that, we can use static queries on our client and also let the query engine of the GraphQL server optimize this static query for execution. Further, we can write a resolver that is optimized to fetch data in one go. Offering plural fields allows for more flexibility and better performance. - -The second aspect here is to be more specific about our fields. The name `speaker` is quite unspecific, and we are already starting to get a problem with this once we introduce a plural version of it called `speakers` since we already have a field `speakers` that is the list of speakers. A good choice in `GraphQL` would be to name the fields `speakerById` and the second one, `speakersById`. - -In this section, we will optimize our `Query` type by bringing in more fields to query our API. Also we will restructure our query type to offer plural versions of our fields that fetch by id. - -1. Head over to your `SpeakerQueries` class and update the `speaker` field to be named `speakerById`. - - ```csharp - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.DataLoader; - using HotChocolate; - using HotChocolate.Types; - using HotChocolate.Types.Relay; - - namespace ConferencePlanner.GraphQL.Speakers - { - [ExtendObjectType("Query")] - public class SpeakerQueries - { - [UseApplicationDbContext] - public Task> GetSpeakersAsync( - [ScopedService] ApplicationDbContext context) => - context.Speakers.ToListAsync(); - - public Task GetSpeakerByIdAsync( - [ID(nameof(Speaker))]int id, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - dataLoader.LoadAsync(id, cancellationToken); - } - } - ``` - -1. Next, introduce a new `GetSpeakersByIdAsync` method as our plural version. - - > Note that the `DataLoader` can also fetch multiples for us. - - ```csharp - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.DataLoader; - using HotChocolate; - using HotChocolate.Types; - using HotChocolate.Types.Relay; - - namespace ConferencePlanner.GraphQL.Speakers - { - [ExtendObjectType("Query")] - public class SpeakerQueries - { - [UseApplicationDbContext] - public Task> GetSpeakersAsync( - [ScopedService] ApplicationDbContext context) => - context.Speakers.ToListAsync(); - - public Task GetSpeakerByIdAsync( - [ID(nameof(Speaker))]int id, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - dataLoader.LoadAsync(id, cancellationToken); - - public async Task> GetSpeakersByIdAsync( - [ID(nameof(Speaker))]int[] ids, - SpeakerByIdDataLoader dataLoader, - CancellationToken cancellationToken) => - await dataLoader.LoadAsync(ids, cancellationToken); - } - } - ``` - -1. Add a new class `SessionQueries` to the `Sessions` directory with the following code: - - ```csharp - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using HotChocolate; - using HotChocolate.Types; - using HotChocolate.Types.Relay; - using ConferencePlanner.GraphQL.DataLoader; - - namespace ConferencePlanner.GraphQL.Sessions - { - [ExtendObjectType("Query")] - public class SessionQueries - { - [UseApplicationDbContext] - public async Task> GetSessionsAsync( - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - await context.Sessions.ToListAsync(cancellationToken); - - public Task GetSessionByIdAsync( - [ID(nameof(Session))] int id, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - sessionById.LoadAsync(id, cancellationToken); - - public async Task> GetSessionsByIdAsync( - [ID(nameof(Session))] int[] ids, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - await sessionById.LoadAsync(ids, cancellationToken); - } - } - ``` - -1. Register the `SessionQueries` with the schema builder which is located in the `Startup.cs` - - ```csharp - services - .AddGraphQLServer() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport(); - ``` - -1. Next add a class `TrackQueries` to the `Tracks` directory with the following code: - - ```csharp - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.DataLoader; - using HotChocolate; - using HotChocolate.Types; - using HotChocolate.Types.Relay; - - namespace ConferencePlanner.GraphQL.Tracks - { - [ExtendObjectType("Query")] - public class TrackQueries - { - [UseApplicationDbContext] - public async Task> GetTracksAsync( - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - await context.Tracks.ToListAsync(cancellationToken); - - [UseApplicationDbContext] - public Task GetTrackByNameAsync( - string name, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - context.Tracks.FirstAsync(t => t.Name == name); - - [UseApplicationDbContext] - public async Task> GetTrackByNamesAsync( - string[] names, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - await context.Tracks.Where(t => names.Contains(t.Name)).ToListAsync(); - - public Task GetTrackByIdAsync( - [ID(nameof(Track))] int id, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) => - trackById.LoadAsync(id, cancellationToken); - - public async Task> GetTracksByIdAsync( - [ID(nameof(Track))] int[] ids, - TrackByIdDataLoader trackById, - CancellationToken cancellationToken) => - await trackById.LoadAsync(ids, cancellationToken); - } - } - ``` - -1. Again, head over to the `Startup.cs` and register the `TrackQueries` with the schema builder. - - ```csharp - services - .AddGraphQLServer() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport(); - ``` - -1. Start you GraphQL server and verify with Banana Cake Pop that you can use the new queries. - - ```console - dotnet run --project GraphQL - ``` - -## Summary - -We have covered quite a lot in this section. We have learned that GraphQL is designed for efficiency and that many of the schema design principals are designed around this core principle of GraphQL. We looked at how Mutations should be structured and that we should aim to design GraphQL schemas, not around a database schema but instead around our business model. With GraphQL we have such strong capabilities to express our business model that we should look beyond simple crud. Expressively design your schema so that each mutation conveys very clearly what it does. Allow your consumers to fetch single entities or multiple entities without forcing them to aliases. - -[**<< Session #3 - Understanding GraphQL query execution and DataLoader**](3-understanding-dataLoader.md) | [**Session #5 - Understanding middleware >>**](5-understanding-middleware.md) diff --git a/docs/4-understanding-middleware.md b/docs/4-understanding-middleware.md new file mode 100644 index 0000000..3e0adbb --- /dev/null +++ b/docs/4-understanding-middleware.md @@ -0,0 +1,177 @@ +# Understanding middleware + +- [Adding a UseUpper middleware](#adding-a-useupper-middleware) +- [Creating a middleware attribute](#creating-a-middleware-attribute) +- [Middleware order](#middleware-order) +- [Summary](#summary) + +The field middleware is one of the foundational components in Hot Chocolate. Many features that you use, for instance, the `ID` transformation from internal IDs to global object identifiers, are field middleware. Even resolvers are compiled into field middleware. + +All the middleware that are applied to a field are compiled into one delegate that can be executed. Each middleware knows about the next middleware component in its chain and with this can choose to execute logic before it or after it or before _and_ after it. Also, a middleware might skip the next middleware in line by not calling next. + +![Abstract Middleware Flow](images/17-middleware-flow.svg) + +A field middleware can be defined by binding it to a field with the descriptor API: + +```csharp +context.Use(next => async context => +{ + // run some code + + // invoke next middleware component in the chain + await next(context); + + // run some more code +}) +``` + +A resolver pipeline is built by applying middleware in order, meaning that the first declared middleware on the field descriptor is the first one executed in the pipeline. The last middleware in the field resolver pipeline is always the field resolver itself. + +![Middleware Flow with Resolver](images/18-middleware-flow.svg) + +The field resolver middleware will only execute if no result has been produced so far. So, if any middleware has set the `Result` property on the context, the field resolver will be skipped. + +Let's write a small middleware that transforms a string into an upper-case string to better understand how field middleware works. + +```csharp +descriptor.Use(next => async context => +{ + await next(context); + + if (context.Result is string s) + { + context.Result = s.ToUpperInvariant(); + } +}); +``` + +The above middleware first invokes the `next` middleware, and by doing so, gives up control and lets the rest of the pipeline do its job. + +After `next` has finished executing, the middleware checks if the result is a `string`, and if so, it applies `ToUpperInvariant` on that `string` and writes back the updated `string` to `context.Result`. + +![Middleware Flow with ToUpper Middleware and Resolver](images/19-middleware-flow.svg) + +## Adding a UseUpper middleware + +1. Create a new directory named `Extensions`: + + ```shell + mkdir GraphQL/Extensions + ``` + +1. Add a new static class named `ObjectFieldDescriptorExtensions`, with the following code: + + ```csharp + namespace ConferencePlanner.GraphQL.Extensions; + + public static class ObjectFieldDescriptorExtensions + { + public static IObjectFieldDescriptor UseUpperCase(this IObjectFieldDescriptor descriptor) + { + return descriptor.Use(next => async context => + { + await next(context); + + if (context.Result is string s) + { + context.Result = s.ToUpperInvariant(); + } + }); + } + } + ``` + +1. Head over to the `TrackType` class in the `Tracks` directory and use the middleware on the `name` field: + + ```csharp + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field(t => t.Name) + .ParentRequires(nameof(Track.Name)) + .UseUpperCase(); + } + ``` + + Note: When we apply middleware to a field, it becomes asynchronous, and will not be projected by default. To enable projection of this field, we use the `ParentRequires` method to indicate that the `Name` property of the parent type `Track` is required. + +1. Start your server and query your tracks: + + ```shell + dotnet run --project GraphQL + ``` + + ```graphql + { + tracks { + name + } + } + ``` + + The result should correctly present us with the names in upper case. + + ```json + { + "data": { + "tracks": [ + { + "name": "TRACK 1" + }, + { + "name": "TRACK 2" + } + ] + } + } + ``` + +## Creating a middleware attribute + +To use middleware on plain C# types, we can wrap them in so-called descriptor attributes. Descriptor attributes let us intercept the descriptors when the type is inferred. For each descriptor type, there is a specific descriptor attribute base class. For our case, we need to use the `ObjectFieldDescriptorAttribute` base class. + +1. Create a new class named `UseUpperCaseAttribute` in the `Extensions` directory and add the following code: + + ```csharp + using System.Reflection; + using HotChocolate.Types.Descriptors; + + namespace ConferencePlanner.GraphQL.Extensions; + + public sealed class UseUpperCaseAttribute : ObjectFieldDescriptorAttribute + { + protected override void OnConfigure( + IDescriptorContext context, + IObjectFieldDescriptor descriptor, + MemberInfo member) + { + descriptor.UseUpperCase(); + } + } + ``` + + This new attribute can now be applied to any property or method on a plain C# type. + + ```csharp + public sealed class Foo + { + [UseUpperCase] + public required string Bar { get; init; } + } + ``` + +## Middleware order + +The following diagram shows the complete field request pipeline with filtering and pagination. You can see how existing middleware are ordered. You have full control over how to order middleware or inject new custom middleware as necessary for your scenarios. + +![Filter Middleware Flow](images/20-middleware-flow.svg) + +The thing here is that if you take for instance `UseFiltering` and `UsePaging`, it would make no sense to first apply paging and basically trim the result in order to then apply filters onto that trimmed result set, the other way around however makes perfect sense. + +That also means that the order of middleware attributes is important, since they form the request pipeline. + +## Summary + +In this session, we've looked at what field middleware are, and how we can use them to add additional processing logic to our field resolver pipeline. + +[**<< Session #3 - GraphQL schema design**](3-schema-design.md) | [**Session #5 - Adding complex filter capabilities >>**](5-adding-complex-filter-capabilities.md) diff --git a/docs/5-adding-complex-filter-capabilities.md b/docs/5-adding-complex-filter-capabilities.md new file mode 100644 index 0000000..73560f9 --- /dev/null +++ b/docs/5-adding-complex-filter-capabilities.md @@ -0,0 +1,300 @@ +# Adding complex filter capabilities + +- [Adding paging to your lists](#adding-paging-to-your-lists) +- [Adding filter capabilities to the top-level field `sessions`](#adding-filter-capabilities-to-the-top-level-field-sessions) +- [Summary](#summary) + +So far, our GraphQL server only exposes plain lists that would, at some point, grow so large that our server would time out. Moreover, we are missing some filter capabilities for our session list so that the application using our backend can filter by title, or search the abstract for topics. + +## Adding paging to your lists + +Let's start by implementing the 2nd Relay server specification by adding Relay-compliant paging to our lists. In general, you should avoid plain lists wherever lists grow or are very large. Relay describes cursor-based paging where you can navigate between edges through their cursors. Cursor-based paging is ideal whenever you implement infinite scrolling solutions. In contrast to offset pagination, you cannot jump to a specific page, but you can jump to a particular cursor and navigate from there. + +1. Add a reference to the NuGet package `HotChocolate.Data.EntityFramework` version `15.0.3`: + - `dotnet add GraphQL package HotChocolate.Data.EntityFramework --version 15.0.3` + +1. Add the cursor paging provider to the schema configuration in `Program.cs`: + + ```diff + .AddMutationConventions() + + .AddDbContextCursorPagingProvider() + .AddGraphQLTypes(); + ``` + + This will add a cursor paging provider that uses native [keyset pagination](https://use-the-index-luke.com/no-offset). + +1. Head over to the `Tracks` directory and replace the `GetTracksAsync` resolver in the `TrackQueries.cs` file with the following code: + + ```csharp + [UsePaging] + public static IQueryable GetTracks(ApplicationDbContext dbContext) + { + return dbContext.Tracks.AsNoTracking().OrderBy(t => t.Name).ThenBy(t => t.Id); + } + ``` + + The new resolver will return an `IQueryable` instead of executing the database query. The `IQueryable` is like a query builder. By applying the `UsePaging` middleware, we are rewriting the database query to only fetch the items that we need for our dataset. + + > Note: In order to use keyset pagination, we must always include a unique column in the ORDER BY clause (in this case, we also order by the primary key `Id`). + + The resolver pipeline for our field now looks like the following: + + ![Paging Middleware Flow](images/22-pagination.svg) + +1. Start your GraphQL server: + + ```shell + dotnet run --project GraphQL + ``` + +1. Open Nitro, refresh the schema, and select the `Schema Reference` tab to see how our API structure has changed. + + ![Nitro Tracks Field](images/24-bcp-schema.webp) + +1. Define a simple query to fetch the first track: + + ```graphql + query GetTrack { + tracks(first: 1) { + edges { + node { + id + name + } + cursor + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + } + } + ``` + + ![Query first track](images/25-bcp-get-first-track.webp) + +1. Take the cursor from this item and add a second argument `after`, with the value of the cursor: + + ```graphql + query GetTrack { + tracks(first: 1, after: "MA==") { + edges { + node { + id + name + } + cursor + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + } + } + ``` + + ![Query next track](images/26-bcp-get-next-track.webp) + +1. Head over to the `SpeakerQueries.cs` file which is located in the `Speakers` directory, and replace the `GetSpeakersAsync` resolver with the following code: + + ```csharp + [UsePaging] + public static IQueryable GetSpeakers(ApplicationDbContext dbContext) + { + return dbContext.Speakers.AsNoTracking().OrderBy(s => s.Name).ThenBy(s => s.Id); + } + ``` + +1. Next, go to the `SessionQueries.cs` file in the `Sessions` directory, and replace the `GetSessionsAsync` resolver with the following code: + + ```csharp + [UsePaging] + public static IQueryable GetSessions(ApplicationDbContext dbContext) + { + return dbContext.Sessions.AsNoTracking().OrderBy(s => s.Title).ThenBy(s => s.Id); + } + ``` + + We have now replaced all the root level list fields and are now using our pagination middleware. There are still more lists left where we should apply pagination if we want to really have a refined schema. Let's change the API a bit more to incorporate this. + +1. Add paging arguments to the schema configuration in `Program.cs`: + + ```diff + .AddDbContextCursorPagingProvider() + + .AddPagingArguments() + .AddGraphQLTypes(); + ``` + + This will make paging arguments available to resolver methods. + +1. Next, open the `TrackType.cs` file in the `Tracks` directory, and replace the `GetSessionsAsync` method with the following code: + + ```csharp + [UsePaging] + public static async Task> GetSessionsAsync( + [Parent] Track track, + ISessionsByTrackIdDataLoader sessionsByTrackId, + PagingArguments pagingArguments, + ISelection selection, + CancellationToken cancellationToken) + { + return await sessionsByTrackId + .With(pagingArguments) + .Select(selection) + .LoadAsync(track.Id, cancellationToken) + .ToConnectionAsync(); + } + ``` + + ```diff + using HotChocolate.Execution.Processing; + + using HotChocolate.Types.Pagination; + ``` + + Here, we apply the `[UsePaging]` attribute, and forward the paging arguments to the DataLoader. We also convert the returned `Page` to a `Connection`. + +1. Next, open the `TrackDataLoaders.cs` file in the `Tracks` directory, and replace the `SessionsByTrackIdAsync` method with the following code: + + ```csharp + [DataLoader] + public static async Task>> SessionsByTrackIdAsync( + IReadOnlyList trackIds, + ApplicationDbContext dbContext, + ISelectorBuilder selector, + PagingArguments pagingArguments, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => s.TrackId != null && trackIds.Contains((int)s.TrackId)) + .OrderBy(s => s.Id) + .Select(s => s.TrackId, selector) + .ToBatchPageAsync(s => (int)s.TrackId!, pagingArguments, cancellationToken); + } + ``` + + Here, we use the `ToBatchPageAsync` method to return a page of sessions for each track ID. + +1. Now go back to Nitro and refresh the schema. + + ![Inspect Track Sessions](images/27-bcp-schema.webp) + +1. Fetch a specific track and get the first session of this track: + + ```graphql + query GetTrackWithSessions { + trackById(id: "VHJhY2s6MQ==") { + id + sessions(first: 1) { + nodes { + title + } + } + } + } + ``` + + ![Query track with sessions](images/28-bcp-get-track-with-sessions.webp) + +## Adding filter capabilities to the top-level field `sessions` + +Exposing rich filters to a public API can lead to unpredictable performance implications, but using filters wisely on select fields can make your API much better to use. In our conference API it would make almost no sense to expose filters on top of the `tracks` field since the `Track` type really only has one field `name`, and filtering on that really seems overkill. The `sessions` field on the other hand could be improved with filter capabilities. The user of our conference app could search for a session with a specific title or in a specific time window. + +Filtering, like paging, is a middleware that can be applied on `IQueryable`. As mentioned in the middleware session, order is important with middleware. This means that our paging middleware has to execute last. + +![Filter Middleware Flow](images/20-middleware-flow.svg) + +1. Add a reference to the NuGet package `HotChocolate.Data` version `15.0.3`: + - `dotnet add GraphQL package HotChocolate.Data --version 15.0.3` + +1. Add filtering and sorting conventions to the schema configuration in `Program.cs`: + + ```diff + .AddPagingArguments() + + .AddFiltering() + + .AddSorting() + .AddGraphQLTypes(); + ``` + +1. Head over to the `SessionQueries.cs` file which is located in the `Sessions` directory. + +1. Replace the `GetSessions` resolver with the following code: + + ```csharp + [UsePaging] + [UseFiltering] + [UseSorting] + public static IQueryable GetSessions(ApplicationDbContext dbContext) + { + return dbContext.Sessions.AsNoTracking().OrderBy(s => s.Title).ThenBy(s => s.Id); + } + ``` + + > By default, the filter middleware would infer a filter type that exposes all the fields of the entity. In our case, it would be better to be explicit, by specifying exactly which fields our users can filter by. + +1. Create a new `SessionFilterInputType.cs` file in the `Sessions` directory, with the following code: + + ```csharp + using ConferencePlanner.GraphQL.Data; + using HotChocolate.Data.Filters; + + namespace ConferencePlanner.GraphQL.Sessions; + + public sealed class SessionFilterInputType : FilterInputType + { + protected override void Configure(IFilterInputTypeDescriptor descriptor) + { + descriptor.BindFieldsExplicitly(); + + descriptor.Field(s => s.Title); + descriptor.Field(s => s.Abstract); + descriptor.Field(s => s.StartTime); + descriptor.Field(s => s.EndTime); + } + } + ``` + + We use the descriptor to set the binding mode to explicit, and to expose the specific fields that we're interested in. + +1. Start your GraphQL server: + + ```shell + dotnet run --project GraphQL + ``` + +1. Open Nitro, refresh the schema, and select the `Schema Reference` tab. + + ![Session Filter Type](images/29-bcp-filter-type.webp) + + > We now have an argument named `where` on our field that exposes a rich filter type. + +1. Write the following query to look for the session with the title `Session 2`: + + ```graphql + query GetSession2 { + sessions( + first: 1 + where: { + title: { eq: "Session 2" } + } + ) { + nodes { + title + } + } + } + ``` + + ![Apply Filter on Sessions](images/30-bcp-get-session2.webp) + +## Summary + +With cursor-based pagination, we've introduced a strong pagination concept and also put the last piece in to be fully Relay compliant. We've learned that we can page within a paged result; in fact, we can create large paging hierarchies. + +Further, we've looked at filtering where we can apply a simple middleware that infers from our data model a powerful filter structure. Filters are rewritten into native database queries on top of `IQueryable` but can also be applied to in-memory lists. Use filters where they make sense, and control them by providing filter types that limit what a user can do, to keep performance predictable. + +[**<< Session #4 - Understanding middleware**](4-understanding-middleware.md) | [**Session #6 - Adding real-time functionality with subscriptions >>**](6-subscriptions.md) diff --git a/docs/5-understanding-middleware.md b/docs/5-understanding-middleware.md deleted file mode 100644 index 935b0d7..0000000 --- a/docs/5-understanding-middleware.md +++ /dev/null @@ -1,169 +0,0 @@ -- [Understanding middleware](#understanding-middleware) - - [Add UseUpper middleware](#add-useupper-middleware) - - [Create middleware attribute](#create-middleware-attribute) - - [Middleware order](#middleware-order) - - [Summary](#summary) - -# Understanding middleware - -The field middleware is one of the foundational components in Hot Chocolate. Many features that you use like, for instance, the `ID` transformation from internal IDs to global object identifiers, are a field middleware. Even resolvers are compiled into a field middleware. - -All the middleware that are applied to a field are compiled into one delegate that can be executed. Each middleware knows about the next middleware component in its chain and with this can choose to execute logic before it or after it or before and after it. Also, a middleware might skip the next middleware in line by not calling next. - -![Abstract Middleware Flow](images/17-middleware-flow.png) - -A field middleware can be defined by binding it to a field with the descriptor API: - -```csharp -context.Use(next => async context => -{ - // do some logic - - // invoke next middleware component in the chain. - await next(context); - - - // do some more logic -}) -``` - -A resolver pipeline is built by applying middleware in order, meaning that the first declared middleware on the field descriptor is the first one executed in the pipeline. The last middleware in the field resolver pipeline is always the field resolver itself. - -![Middleware Flow with Resolver](images/18-middleware-flow.png) - -The field resolver middleware will only execute if no result has been produced so far. So, if any middleware has set the `Result` property on the context, the field resolver will be skipped. - -Let us write a little middleware that makes a string into an all upper case string to understand better how field middleware works. - -```csharp -descriptor.Use(next => async context => -{ - await next(context); - - if (context.Result is string s) - { - context.Result = s.ToUpperInvariant(); - } -}); -``` - -The above middleware first invokes the `next` middleware, and by doing so, gives up control and lets the rest of the pipeline do its job. - -After `next` has finished executing, the middleware checks if the result is a `string`, and if so, it applies a `ToUpperInvariant` on that `string` and writes back the updated `string` to `context.Result`. - -![Middleware Flow with ToUpper Middleware and Resolver](images/19-middleware-flow.png) - -## Add UseUpper middleware - -1. Head over to the `ObjectFieldDescriptorExtensions` class located in the `Extensions` folder. - -1. Add the `UseUpperCase` extension method to the `ObjectFieldDescriptorExtensions` class. - - ```csharp - public static IObjectFieldDescriptor UseUpperCase( - this IObjectFieldDescriptor descriptor) - { - return descriptor.Use(next => async context => - { - await next(context); - - if (context.Result is string s) - { - context.Result = s.ToUpperInvariant(); - } - }); - } - ``` - -1. Head over to the `TrackType` in the `Types` folder and use the middleware on the `name` field. - - ```csharp - descriptor - .Field(t => t.Name) - .UseUpperCase(); - ``` - -1. Start your server and query your tracks. - - ```console - dotnet run --project GraphQL - ``` - - ```graphql - { - tracks { - name - } - } - ``` - - The result should correctly present us with the name all in upper-case. - - ```json - { - "data": { - "tracks": [ - { - "name": "TRACK 1" - }, - { - "name": "TRACK 2" - } - ] - } - } - ``` - -## Create middleware attribute - -To use middleware on plain C# types, we can wrap them in so-called descriptor attributes. Descriptor attributes let us intercept the descriptors when the type is inferred. For each descriptor type, there is a specific descriptor attribute base class. For our case, we need to use the `ObjectFieldDescriptorAttribute` base class. - -1. Create a new class `UseUpperCaseAttribute` in the `Extensions` directory and add the following code: - - ```csharp - using HotChocolate.Types; - using HotChocolate.Types.Descriptors; - using System.Reflection; - - namespace ConferencePlanner.GraphQL - { - public class UseUpperCaseAttribute : ObjectFieldDescriptorAttribute - { - public override void OnConfigure( - IDescriptorContext context, - IObjectFieldDescriptor descriptor, - MemberInfo member) - { - descriptor.UseUpperCase(); - } - } - } - ``` - - > This new attribute can now be applied to any property or method on a plain C# type. - - > ```csharp - > public class Foo - > { - > [UseUpperCase] - > public string Bar { get; set; } - > } - > ``` - -## Middleware order - -The following diagram shows the complete field request pipeline with filtering and pagination. You can see how, existing middleware are ordered. You have full control over how to order middleware or inject new custom middleware as necessary for your scenarios. - -![Filter Middleware Flow](images/20-middleware-flow.png) - -The thing here is that if you take for instance UseFiltering and UsePaging, it would make no sense to first apply paging and basically trim the result in order to then apply filters onto that trimmed result set, the other way around however makes perfect sense. - -**Middleware order is important!** - -That also means that the order of middleware attributes is important since they form the request pipeline. - -## Summary - -In this session, we have looked at what field middleware are and how we can use them to add additional processing logic to our field resolver pipeline. - -[**<< Session #4 - GraphQL schema design**](4-schema-design.md) | [**Session #6 - Adding complex filter capabilities >>**](6-adding-complex-filter-capabilities.md) diff --git a/docs/6-adding-complex-filter-capabilities.md b/docs/6-adding-complex-filter-capabilities.md deleted file mode 100644 index 9202615..0000000 --- a/docs/6-adding-complex-filter-capabilities.md +++ /dev/null @@ -1,279 +0,0 @@ -- [Adding complex filter capabilities](#adding-complex-filter-capabilities) - - [Add paging to your lists](#add-paging-to-your-lists) - - [Add filter capabilities to the top-level field `sessions`](#add-filter-capabilities-to-the-top-level-field-sessions) - - [Summary](#summary) - -# Adding complex filter capabilities - -So far, our GraphQL server only exposes plain lists that would, at some point, grow so large that our server would time out. Moreover, we miss some filter capabilities for our session list so that the application using our backend can filter for tracks, titles, or search the abstract for topics. - -## Add paging to your lists - -Let us start by implementing the last Relay server specification we are still missing in our server by adding Relay compliant paging to our lists. In general, you should avoid plain lists wherever lists grow or are very large. Relay describes a cursor based paging where you can navigate between edges through their cursors. Cursor based paging is ideal whenever you implement infinite scrolling solutions. In contrast to offset-pagination, you cannot jump to a specific page, but you can jump to a particular cursor and navigate from there. - -> Many database drivers or databases do not support `skip while`, so Hot Chocolate will under the hood use positions instead of proper IDs for cursors in theses cases. Meaning, you can always use cursor-based pagination, and Hot Chocolate will handle the rest underneath. - -1. Head over to the `Tracks`directory and replace the `GetTracksAsync` resolver in the `TrackQueries.cs` with the following code. - - ```csharp - [UseApplicationDbContext] - [UsePaging] - public IQueryable GetTracks( - [ScopedService] ApplicationDbContext context) => - context.Tracks.OrderBy(t => t.Name); - ``` - - > The new resolver will instead of executing the database query return an `IQueryable`. The `IQueryable` is like a query builder. By applying the `UsePaging` middleware, we are rewriting the database query to only fetch the items that we need for our data-set. - - The resolver pipeline for our field now looks like the following: - - ![Paging Middleware Flow](images/22-pagination.png) - -1. Start your GraphQL server. - - ```console - dotnet run --project GraphQL - ``` - -1. Open Banana Cake Pop and refresh the schema. - - ![Banana Cake Pop Root Fields](images/23-bcp-schema.png) - -1. Head into the schema browser, and let us have a look at how our API structure has changed. - - ![Banana Cake Pop Tracks Field](images/24-bcp-schema.png) - -1. Define a simple query to fetch the first track. - - ```graphql - query GetFirstTrack { - tracks(first: 1) { - edges { - node { - id - name - } - cursor - } - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - } - } - ``` - - ![Query speaker names](images/25-bcp-GetFirstTrack.png) - -1. Take the cursor from this item and add a second argument after and feed in the cursor. - - ```graphql - query GetNextItem { - tracks(first: 1, after: "MA==") { - edges { - node { - id - name - } - cursor - } - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - } - } - ``` - - ![Query speaker names](images/26-bcp-GetNextTrack.png) - -1. Head over to the `SpeakerQueries.cs` which are located in the `Speakers` directory and replace the `GetSpeakersAsync` resolver with the following code: - - ```csharp - [UseApplicationDbContext] - [UsePaging] - public IQueryable GetSpeakers( - [ScopedService] ApplicationDbContext context) => - context.Speakers.OrderBy(t => t.Name); - ``` - -1. Next, go to the `SessionQueries.cs` in the `Sessions` directory and replace the `GetSessionsAsync` with the following code: - - ```csharp - [UseApplicationDbContext] - [UsePaging] - public IQueryable GetSessions( - [ScopedService] ApplicationDbContext context) => - context.Sessions; - ``` - - We have now replaced all the root level list fields and are now using our pagination middleware. There are still more lists left where we should apply pagination if we wanted to really have a refined schema. Let us change the API a bit more to incorporate this. - -1. First, go back to the `SessionQueries.cs` in the `Sessions` directory and replace the `[UsePaging]` with `[UsePaging(typeof(NonNullType))]`. - - ```csharp - [UseApplicationDbContext] - [UsePaging(typeof(NonNullType))] - public IQueryable GetSessions( - [ScopedService] ApplicationDbContext context) => - context.Sessions; - ``` - - > It is important that a connection type works with a fixed item type if we mix attribute and fluent syntax. - -1. Next, open the `TrackType.cs` in the `Types` directory and add `.UsePaging>()` to the `Sessions` field descriptor. - - ```csharp - descriptor - .Field(t => t.Sessions) - .ResolveWith(t => t.GetSessionsAsync(default!, default!, default!, default)) - .UseDbContext() - .UsePaging>() - .Name("sessions"); - ``` - -1. Now go back to Banana Cake Pop and refresh the schema. - - ![Inspect Track Session](images/27-bcp-schema.png) - -1. Fetch a specific track and get the first session of this track: - - ```graphql - query GetTrackWithSessions { - trackById(id: "VHJhY2sKaTI=") { - id - sessions(first: 1) { - nodes { - title - } - } - } - } - ``` - - ![Query speaker names](images/28-bcp-GetTrackWithSessions.png) - - > There is one caveat in our implementation with the `TrackType`. Since, we are using a DataLoader within our resolver and first fetch the list of IDs we essentially will always fetch everything and chop in memory. In an actual project this can be split into two actions by moving the `DataLoader` part into a middleware and first page on the id queryable. Also one could implement a special `IPagingHandler` that uses the DataLoader and applies paging logic. - -## Add filter capabilities to the top-level field `sessions` - -Exposing rich filters to a public API can lead to unpredictable performance implications, but using filters wisely on select fields can make your API much better to use. In our conference API it would make almost no sense to expose filters on top of the `tracks` field since the `Track` type really only has one field `name` and filtering on that really seems overkill. The `sessions` field on the other hand could be improved with filter capabilities. The user of our conference app could with filters search for a session in a specific time-window or for sessions of a specific speaker he/she likes. - -Filters like paging is a middleware that can be applied on `IQueryable`, like mentioned in the middleware session order is important with middleware. This means our paging middleware has to execute last. - -![Filter Middleware Flow](images/20-middleware-flow.png) - -1. Add a reference to the NuGet package package `HotChocolate.Data` version `11.0.0`. - - 1. `dotnet add GraphQL package HotChocolate.Data --version 11.0.0` - -1. Add filter and sorting conventions to the schema configuration. - - ```csharp - services - .AddGraphQLServer() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport() - .AddFiltering() - .AddSorting() - .AddDataLoader() - .AddDataLoader(); - ``` - -1. Head over to the `SessionQueries.cs` which is located in the `Sessions` directory. - -1. Replace the `GetSessions` resolver with the following code: - - ```csharp - [UseApplicationDbContext] - [UsePaging(typeof(NonNullType))] - [UseFiltering] - [UseSorting] - public IQueryable GetSessions( - [ScopedService] ApplicationDbContext context) => - context.Sessions; - ``` - - > By default the filter middleware would infer a filter type that exposes all the fields of the entity. In our case it would be better to remove filtering for ids and internal fields and focus on fields that the user really can use. - -1. Create a new `SessionFilterInputType.cs` in the `Sessions` directory with the following code: - - ```csharp - using ConferencePlanner.GraphQL.Data; - using HotChocolate.Data.Filters; - - namespace ConferencePlanner.GraphQL.Types - { - public class SessionFilterInputType : FilterInputType - { - protected override void Configure(IFilterInputTypeDescriptor descriptor) - { - descriptor.Ignore(t => t.Id); - descriptor.Ignore(t => t.TrackId); - } - } - } - ``` - - > We essentially have remove the ID fields and leave the rest in. - -1. Go back to the `SessionQueries.cs` which is located in the `Sessions` directory and replace the `[UseFiltering]` attribute on top of the `GetSessions` resolver with the following `[UseFiltering(typeof(SessionFilterInputType))]`. - - ```csharp - [UseApplicationDbContext] - [UsePaging(typeof(NonNullType))] - [UseFiltering(typeof(SessionFilterInputType))] - [UseSorting] - public IQueryable GetSessions( - [ScopedService] ApplicationDbContext context) => - context.Sessions; - ``` - -1. Start your GraphQL server. - - ```console - dotnet run --project GraphQL - ``` - -1. Open Banana Cake Pop and refresh the schema and head over to the schema browser. - - ![Session Filter Type](images/29-bcp-filter-type.png) - - > We now have an argument `where` on our field that exposes a rich filter type to us. - -1. Write the following query to look for all the sessions that contain `2` in their title. - - ```graphql - query GetSessionsContaining2InTitle { - sessions(where: { title: { contains: "2" } }) { - nodes { - title - } - } - } - ``` - - ![Apply Filter on Sessions](images/30-bcp-get-sessions.png) - -## Summary - -With cursor base pagination, we have introduced a strong pagination concept and also put the last piece in to be fully Relay compliant. We have learned that we can page within a paged result; in fact, you can create large paging hierarchies. - -Further, we have looked at filtering where we can apply a simple middleware that infers from our data model a powerful filter structure. Filters are rewritten into native database queries on top of `IQueryable` but can also be applied to in-memory lists. Use filters where they make sense and control them by providing filter types that limit what a user can do to keep performance predictable. - -[**<< Session #5 - Understanding middleware**](5-understanding-middleware.md) | [**Session #7 - Adding real-time functionality with subscriptions >>**](7-subscriptions.md) diff --git a/docs/6-subscriptions.md b/docs/6-subscriptions.md new file mode 100644 index 0000000..0848056 --- /dev/null +++ b/docs/6-subscriptions.md @@ -0,0 +1,580 @@ +# Adding real-time functionality with subscriptions + +- [Adding to our GraphQL API](#adding-to-our-graphql-api) + - [Adding a `registerAttendee` mutation](#adding-a-registerattendee-mutation) + - [Adding a `checkInAttendee` mutation](#adding-a-checkinattendee-mutation) +- [Adding an `onSessionScheduled` subscription](#adding-an-onsessionscheduled-subscription) +- [Adding an `onAttendeeCheckedIn` subscription](#adding-an-onattendeecheckedin-subscription) +- [Summary](#summary) + +For the last few parts of our journey through GraphQL, we've dealt with queries and mutations. In many APIs, this is all that people need or want, but GraphQL also offers us real-time capabilities where we can formulate what data we want to receive when a specific event occurs. + +For our conference API, we'd like to introduce two events that a user can subscribe to. Firstly, whenever a session is scheduled, we want to be notified. An `onSessionScheduled` event would allow us to send the user notifications whenever a new session is available, or whenever a schedule for a specific session has changed. + +The second case that we have for subscriptions is whenever a user checks in to a session, we want to raise a subscription so that we can notify users that the space in a session is running low or even have some analytics tool subscribe to this event. + +## Adding to our GraphQL API + +Before we can start with introducing our new subscriptions, we need to first bring in some new types. + +1. Create a new class named `AttendeeQueries`, in the `Attendees` directory, with the following content: + + ```csharp + using ConferencePlanner.GraphQL.Data; + using GreenDonut.Data; + using HotChocolate.Execution.Processing; + using Microsoft.EntityFrameworkCore; + + namespace ConferencePlanner.GraphQL.Attendees; + + [QueryType] + public static class AttendeeQueries + { + [UsePaging] + public static IQueryable GetAttendees(ApplicationDbContext dbContext) + { + return dbContext.Attendees.AsNoTracking().OrderBy(a => a.Username); + } + + [NodeResolver] + public static async Task GetAttendeeByIdAsync( + int id, + IAttendeeByIdDataLoader attendeeById, + ISelection selection, + CancellationToken cancellationToken) + { + return await attendeeById.Select(selection).LoadAsync(id, cancellationToken); + } + + public static async Task> GetAttendeesByIdAsync( + [ID] int[] ids, + IAttendeeByIdDataLoader attendeeById, + ISelection selection, + CancellationToken cancellationToken) + { + return await attendeeById.Select(selection).LoadRequiredAsync(ids, cancellationToken); + } + } + ``` + +### Adding a `registerAttendee` mutation + +We now have the base types integrated and can start adding the attendee mutations. We'll begin by adding in the `registerAttendee` mutation. + +1. Add a new class named `RegisterAttendeeInput` to the `Attendees` directory: + + ```csharp + namespace ConferencePlanner.GraphQL.Attendees; + + public sealed record RegisterAttendeeInput( + string FirstName, + string LastName, + string Username, + string EmailAddress); + ``` + +1. Add an `AttendeeMutations` class with a `RegisterAttendeeAsync` resolver to the `Attendees` directory: + + ```csharp + using ConferencePlanner.GraphQL.Data; + + namespace ConferencePlanner.GraphQL.Attendees; + + [MutationType] + public static class AttendeeMutations + { + public static async Task RegisterAttendeeAsync( + RegisterAttendeeInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var attendee = new Attendee + { + FirstName = input.FirstName, + LastName = input.LastName, + Username = input.Username, + EmailAddress = input.EmailAddress + }; + + dbContext.Attendees.Add(attendee); + + await dbContext.SaveChangesAsync(cancellationToken); + + return attendee; + } + } + ``` + +### Adding a `checkInAttendee` mutation + +Now that we have the mutation in to register new attendees, let's move on to adding another mutation that will allow us to check a user into a session. + +1. Add a `CheckInAttendeeInput` record to the `Attendees` directory: + + ```csharp + using ConferencePlanner.GraphQL.Data; + + namespace ConferencePlanner.GraphQL.Attendees; + + public sealed record CheckInAttendeeInput( + [property: ID] int SessionId, + [property: ID] int AttendeeId); + ``` + +1. Add an `AttendeeExceptions.cs` file to the `Attendees` directory, with the following code: + + ```csharp + namespace ConferencePlanner.GraphQL.Attendees; + + public sealed class AttendeeNotFoundException() : Exception("Attendee not found."); + ``` + +1. Head back to the `AttendeeMutations` class in the `Attendees` directory, and add the `CheckInAttendeeAsync` resolver to it: + + ```csharp + public static async Task CheckInAttendeeAsync( + CheckInAttendeeInput input, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var attendee = await dbContext.Attendees.FirstOrDefaultAsync( + a => a.Id == input.AttendeeId, + cancellationToken); + + if (attendee is null) + { + throw new AttendeeNotFoundException(); + } + + attendee.SessionsAttendees.Add(new SessionAttendee { SessionId = input.SessionId }); + + await dbContext.SaveChangesAsync(cancellationToken); + + return attendee; + } + ``` + +1. Start your GraphQL server: + + ```shell + dotnet run --project GraphQL + ``` + +1. Validate that you see your new queries and mutations with Nitro. + +## Adding an `onSessionScheduled` subscription + +With the base in, we can now focus on putting subscriptions in our GraphQL server. GraphQL subscriptions by default work over WebSockets but could also work over SignalR or gRPC. We'll first update our request pipeline to use WebSockets, and then we'll set up the subscription pub/sub system. After having our server prepared, we'll add the subscriptions to our API. + +1. Update the `docker-compose.yml` file with a new Redis service: + + ```yaml + graphql-workshop-redis: + container_name: graphql-workshop-redis + image: redis:7.4 + networks: [graphql-workshop] + ports: [6379:6379] + volumes: + - type: volume + source: redis-data + target: /data + ``` + + ```diff + volumes: + postgres-data: + + redis-data: + ``` + +1. Add a reference to the NuGet package `HotChocolate.Subscriptions.Redis` version `15.0.3`: + - `dotnet add GraphQL package HotChocolate.Subscriptions.Redis --version 15.0.3` + +1. Head over to `Program.cs` and add `app.UseWebSockets()` to the request pipeline. Middleware order is also important with ASP.NET Core, so this middleware needs to come before the GraphQL middleware: + + ```diff + + app.UseWebSockets(); + app.MapGraphQL(); + ``` + +1. Stay in the `Program.cs` file and add Redis subscriptions to the GraphQL configuration: + + ```diff + .AddSorting() + + .AddRedisSubscriptions(_ => ConnectionMultiplexer.Connect("127.0.0.1:6379")) + .AddGraphQLTypes(); + ``` + + With `app.UseWebSockets()` we've enabled our server to handle websocket requests. With `AddRedisSubscriptions(...)` we've added a Redis pub/sub system for GraphQL subscriptions to our schema. + +1. Add a new class named `SessionSubscriptions` to the `Sessions` directory: + + ```csharp + using ConferencePlanner.GraphQL.Data; + + namespace ConferencePlanner.GraphQL.Sessions; + + [SubscriptionType] + public static class SessionSubscriptions + { + [Subscribe] + [Topic] + public static async Task OnSessionScheduledAsync( + [EventMessage] int sessionId, + ISessionByIdDataLoader sessionById, + CancellationToken cancellationToken) + { + return await sessionById.LoadRequiredAsync(sessionId, cancellationToken); + } + } + ``` + + The `[Subscribe]` attribute tells the schema builder that this resolver method needs to be hooked up to the pub/sub system. This means that in the background, the resolver compiler will create a so-called subscribe resolver that handles subscribing to the pub/sub system. + + The `[Topic]` attribute can be put on the method or a parameter of the method and will infer the pub/sub topic for this subscription. + + The `[EventMessage]` attribute marks the parameter where the execution engine will inject the message payload of the pub/sub system. + + The subscription type itself is automatically registered, but we still need something to trigger the event. So next, we are going to update our `scheduleSession` resolver to trigger an event. + +1. Head over to the `SessionMutations` class in the `Sessions` directory and replace `ScheduleSessionAsync` with the following code: + + ```csharp + [Error] + [Error] + public static async Task ScheduleSessionAsync( + ScheduleSessionInput input, + ApplicationDbContext dbContext, + ITopicEventSender eventSender, + CancellationToken cancellationToken) + { + if (input.EndTime < input.StartTime) + { + throw new EndTimeInvalidException(); + } + + var session = await dbContext.Sessions.FindAsync([input.SessionId], cancellationToken); + + if (session is null) + { + throw new SessionNotFoundException(); + } + + session.TrackId = input.TrackId; + session.StartTime = input.StartTime; + session.EndTime = input.EndTime; + + await dbContext.SaveChangesAsync(cancellationToken); + + await eventSender.SendAsync( + nameof(SessionSubscriptions.OnSessionScheduledAsync), + session.Id, + cancellationToken); + + return session; + } + ``` + + Our improved resolver now injects `ITopicEventSender eventSender`. This gives us access to send messages to the underlying pub/sub system. + + After `await dbContext.SaveChangesAsync(cancellationToken);`, we are sending a new message. + + ```csharp + await eventSender.SendAsync( + nameof(SessionSubscriptions.OnSessionScheduledAsync), + session.Id, + cancellationToken); + ``` + + Since we added the `[Topic]` attribute on our resolver method in the `SessionSubscriptions` class, the topic is now the name of this method. A topic can be anything that can be serialized and has equality implemented so you could also use an object. + +1. Start your GraphQL server: + + ```shell + dotnet run --project GraphQL + ``` + +1. Open Nitro and refresh the schema. + +1. Open a new query tab and add the following subscription query: + + ```graphql + subscription { + onSessionScheduled { + title + startTime + } + } + ``` + + Execute the subscription query. Nothing will happen at this point, and you'll just see a loading indicator. + + ![Subscription Waiting for Events](images/31-bcp-subscribe.webp) + +1. Open another tab in Nitro and add the following document: + + ```graphql + query GetSessionsAndTracks { + sessions(first: 1) { + nodes { + id + } + } + tracks(first: 1) { + nodes { + id + } + } + } + + mutation ScheduleSession { + scheduleSession( + input: { + sessionId: "U2Vzc2lvbjox" + trackId: "VHJhY2s6MQ==" + startTime: "2020-08-01T16:00:00Z" + endTime: "2020-08-01T17:00:00Z" + } + ) { + session { + title + } + } + } + ``` + + Execute `GetSessionsAndTracks` first by clicking the `Run` link above it. Use the IDs from the response for `ScheduleSession` and execute it once you have filled in the correct IDs. + + ![Subscription Scheduled](images/32-bcp-scheduled.webp) + +1. Return to the first query tab (the tab where you specified the subscription query). + + ![Subscription Result](images/33-bcp-subscription-result.webp) + + The event was raised, and our subscription query was executed. We can also see that the loading indicator is still turning since we are still subscribed, and we'll get new responses whenever the event is raised. With GraphQL a subscription stream can be infinite or finite. A finite stream will automatically complete whenever the server chooses to complete the topic (`ITopicEventSender.CompleteAsync`). + + To stop the subscription from the client side, click on the `Cancel` button. + +## Adding an `onAttendeeCheckedIn` subscription + +The `onSessionScheduled` subscription was quite simple since we didn't subscribe to a dynamic topic. A dynamic topic refers to a topic that is defined at the moment we subscribe to it, or a topic that depends on the user context. With `onAttendeeCheckedIn`, we'll subscribe to a specific session to see who checked in and how quickly it fills up. + +1. Head over to the `AttendeeMutations` class and replace the `CheckInAttendeeAsync` resolver with the following code: + + ```csharp + public static async Task CheckInAttendeeAsync( + CheckInAttendeeInput input, + ApplicationDbContext dbContext, + ITopicEventSender eventSender, + CancellationToken cancellationToken) + { + var attendee = await dbContext.Attendees.FirstOrDefaultAsync( + a => a.Id == input.AttendeeId, + cancellationToken); + + if (attendee is null) + { + throw new AttendeeNotFoundException(); + } + + attendee.SessionsAttendees.Add(new SessionAttendee { SessionId = input.SessionId }); + + await dbContext.SaveChangesAsync(cancellationToken); + + await eventSender.SendAsync( + $"OnAttendeeCheckedIn_{input.SessionId}", + input.AttendeeId, + cancellationToken); + + return attendee; + } + ``` + + In this instance, we are again using our `ITopicEventSender` to send messages to our pub/sub system. However, we are now creating a string topic that includes the session ID. If nobody is subscribed, the messages will just be dropped. + + ```csharp + await eventSender.SendAsync( + $"OnAttendeeCheckedIn_{input.SessionId}", + input.AttendeeId, + cancellationToken); + ``` + +1. Add a new class named `SessionAttendeeCheckIn` to the `Attendees` directory. This will be our subscription payload: + + ```csharp + using Microsoft.EntityFrameworkCore; + using ConferencePlanner.GraphQL.Data; + using ConferencePlanner.GraphQL.Sessions; + + namespace ConferencePlanner.GraphQL.Attendees; + + public sealed class SessionAttendeeCheckIn(int attendeeId, int sessionId) + { + [ID] + public int AttendeeId { get; } = attendeeId; + + [ID] + public int SessionId { get; } = sessionId; + + public async Task CheckInCountAsync( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + return await dbContext.Sessions + .AsNoTracking() + .Where(s => s.Id == SessionId) + .SelectMany(s => s.SessionAttendees) + .CountAsync(cancellationToken); + } + + public async Task GetAttendeeAsync( + IAttendeeByIdDataLoader attendeeById, + CancellationToken cancellationToken) + { + return await attendeeById.LoadRequiredAsync(AttendeeId, cancellationToken); + } + + public async Task GetSessionAsync( + ISessionByIdDataLoader sessionById, + CancellationToken cancellationToken) + { + return await sessionById.LoadRequiredAsync(SessionId, cancellationToken); + } + } + ``` + +1. Create a new class named `AttendeeSubscriptions` and put it in the `Attendees` directory: + + ```csharp + using ConferencePlanner.GraphQL.Data; + using HotChocolate.Execution; + using HotChocolate.Subscriptions; + + namespace ConferencePlanner.GraphQL.Attendees; + + [SubscriptionType] + public static class AttendeeSubscriptions + { + [Subscribe(With = nameof(SubscribeToOnAttendeeCheckedInAsync))] + public static SessionAttendeeCheckIn OnAttendeeCheckedIn( + [ID] int sessionId, + [EventMessage] int attendeeId) + { + return new SessionAttendeeCheckIn(attendeeId, sessionId); + } + + public static async ValueTask> SubscribeToOnAttendeeCheckedInAsync( + int sessionId, + ITopicEventReceiver eventReceiver, + CancellationToken cancellationToken) + { + return await eventReceiver.SubscribeAsync( + $"OnAttendeeCheckedIn_{sessionId}", + cancellationToken); + } + } + ``` + + `OnAttendeeCheckedIn` represents our resolver like in the first subscription that we built, but now in our `Subscribe` attribute we are referring to a method named `SubscribeToOnAttendeeCheckedInAsync`. So, instead of letting the system generate a subscribe resolver that handles subscribing to the pub/sub system, we are creating it ourselves in order to control how it's done, or to filter out events that we don't want to pass down. + + ```csharp + public static async ValueTask> SubscribeToOnAttendeeCheckedInAsync( + int sessionId, + ITopicEventReceiver eventReceiver, + CancellationToken cancellationToken) + { + return await eventReceiver.SubscribeAsync( + $"OnAttendeeCheckedIn_{sessionId}", + cancellationToken); + } + ``` + + The subscribe resolver is using `ITopicEventReceiver` to subscribe to a topic. A subscribe resolver can return `IAsyncEnumerable`, `IEnumerable`, or `IObservable` to represent the subscription stream. The subscribe resolver has access to all of the arguments that the actual resolver has access to. + +1. Start your GraphQL server again: + + ```shell + dotnet run --project GraphQL + ``` + +1. Open a new tab and add the following document: + + ```graphql + query GetSessions { + sessions(first: 1) { + nodes { + id + } + } + } + + mutation RegisterAttendee { + registerAttendee( + input: { + firstName: "Michael" + lastName: "Staib" + username: "michael" + emailAddress: "michael@chillicream.com" + } + ) { + attendee { + id + } + } + } + + mutation CheckInAttendee { + checkInAttendee( + input: { + attendeeId: "QXR0ZW5kZWU6MQ==" + sessionId: "U2Vzc2lvbjox" + } + ) { + attendee { + username + } + } + } + ``` + + Execute `GetSessions` first, take the resulting session ID, and feed it into the `CheckInAttendee` operation. + + ![Execute GetSessions](images/34-bcp-get-sessions.webp) + + Next, execute `RegisterAttendee`, take the resulting attendee ID, and feed it into the `CheckInAttendee` operation. + + ![Execute RegisterAttendee](images/35-bcp-register-attendee.webp) + +1. Open another tab in Nitro and add the following document: + + ```graphql + subscription OnAttendeeCheckedIn { + onAttendeeCheckedIn(sessionId: "U2Vzc2lvbjox") { + checkInCount + attendee { + username + } + } + } + ``` + + Take the session ID that you gathered earlier and pass it into the `sessionId` argument of `OnAttendeeCheckedIn`. + + Execute `OnAttendeeCheckedIn`. Again, nothing will happen at this point, and the query tab is just waiting for incoming messages. + + ![Execute OnAttendeeCheckedIn](images/36-bcp-on-attendee-checked-in.webp) + +1. Go back to the previous tab and execute the `CheckInAttendee` operation. + + ![Execute CheckInAttendee](images/37-bcp-check-in-attendee.webp) + +1. Click on the 2nd tab to verify that we've received the message that an attendee has checked into our session. + + ![OnAttendeeCheckedIn Received Result](images/38-bcp-on-attendee-checked-in.webp) + +## Summary + +In this session, we've learned how we can use GraphQL subscriptions to provide real-time events. GraphQL makes it easy to work with real-time data since we can specify what data we want to receive when an event occurs in our system. + +[**<< Session #5 - Adding complex filter capabilities**](5-adding-complex-filter-capabilities.md) | [**Session #7 - Testing the GraphQL server >>**](7-testing-the-graphql-server.md) + + diff --git a/docs/7-subscriptions.md b/docs/7-subscriptions.md deleted file mode 100644 index 5f317da..0000000 --- a/docs/7-subscriptions.md +++ /dev/null @@ -1,862 +0,0 @@ -- [Adding real-time functionality with subscriptions](#adding-real-time-functionality-with-subscriptions) - - [Refactor GraphQL API](#refactor-graphql-api) - - [Add `registerAttendee` Mutation](#add-registerattendee-mutation) - - [Add `checkInAttendee` Mutation](#add-checkinattendee-mutation) - - [Add `onSessionScheduled` Subscription](#add-onsessionscheduled-subscription) - - [Add `onAttendeeCheckedIn` subscription](#add-onattendeecheckedin-subscription) - - [Summary](#summary) - -# Adding real-time functionality with subscriptions - -For the last few parts of our journey through GraphQL, we have dealt with queries and mutations. In many APIs, this is all people need or want, but GraphQL also offers us real-time capabilities where we can formulate what data we want to receive when a specific event happens. - -For our conference API, we would like to introduce two events a user can subscribe to. So, whenever a session is scheduled, we want to be notified. An `onSessionScheduled` event would allow us to send the user notifications whenever a new session is available or whenever a schedule for a specific session has changed. - -The second case that we have for subscriptions is whenever a user checks in to a session we want to raise a subscription so that we can notify users that the space in a session is running low or even have some analytics tool subscribe to this event. - -## Refactor GraphQL API - -Before we can start with introducing our new subscriptions, we need first to bring in some new types and add some more packages. - -1. Add a new directory `Attendees`. - - ```console - mkdir GraphQL/Attendees - ``` - -1. Create a new class `AttendeeQueries` located in the `Attendees` directory with the following content: - - ```csharp - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.DataLoader; - using HotChocolate; - using HotChocolate.Types; - using HotChocolate.Types.Relay; - using System.Linq; - - namespace ConferencePlanner.GraphQL.Attendees - { - [ExtendObjectType(Name = "Query")] - public class AttendeeQueries - { - [UseApplicationDbContext] - [UsePaging] - public IQueryable GetAttendees( - [ScopedService] ApplicationDbContext context) => - context.Attendees; - - public Task GetAttendeeByIdAsync( - [ID(nameof(Attendee))]int id, - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) => - attendeeById.LoadAsync(id, cancellationToken); - - public async Task> GetAttendeesByIdAsync( - [ID(nameof(Attendee))]int[] ids, - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) => - await attendeeById.LoadAsync(ids, cancellationToken); - } - } - ``` - -1. Add a class `AttendeePayloadBase` to the `Attendees` directory. - - ```csharp - using System.Collections.Generic; - using ConferencePlanner.GraphQL.Common; - using ConferencePlanner.GraphQL.Data; - - namespace ConferencePlanner.GraphQL.Attendees - { - public class AttendeePayloadBase : Payload - { - protected AttendeePayloadBase(Attendee attendee) - { - Attendee = attendee; - } - - protected AttendeePayloadBase(IReadOnlyList errors) - : base(errors) - { - } - - public Attendee? Attendee { get; } - } - } - ``` - -### Add `registerAttendee` Mutation - -We now have the base types integrated and can start adding the attendee mutations. We will begin by adding in the `registerAttendee` Mutation. - -1. Add a new class `RegisterAttendeeInput` to the `Attendees` directory. - - ```csharp - namespace ConferencePlanner.GraphQL.Attendees - { - public record RegisterAttendeeInput( - string FirstName, - string LastName, - string UserName, - string EmailAddress); - } - ``` - -1. Now, add the `RegisterAttendeePayload` class to the `Attendees` directory. - - ```csharp - using ConferencePlanner.GraphQL.Common; - using ConferencePlanner.GraphQL.Data; - - namespace ConferencePlanner.GraphQL.Attendees - { - public class RegisterAttendeePayload : AttendeePayloadBase - { - public RegisterAttendeePayload(Attendee attendee) - : base(attendee) - { - } - - public RegisterAttendeePayload(UserError error) - : base(new[] { error }) - { - } - } - } - ``` - -1. Add the `AttendeeMutations` with the `RegisterAttendeeAsync` resolver to the `Attendees` directory. - - ```csharp - using System.Threading; - using System.Threading.Tasks; - using ConferencePlanner.GraphQL.Common; - using ConferencePlanner.GraphQL.Data; - using HotChocolate; - using HotChocolate.Types; - using HotChocolate.Subscriptions; - - namespace ConferencePlanner.GraphQL.Attendees - { - [ExtendObjectType(Name = "Mutation")] - public class AttendeeMutations - { - [UseApplicationDbContext] - public async Task RegisterAttendeeAsync( - RegisterAttendeeInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - var attendee = new Attendee - { - FirstName = input.FirstName, - LastName = input.LastName, - UserName = input.UserName, - EmailAddress = input.EmailAddress - }; - - context.Attendees.Add(attendee); - - await context.SaveChangesAsync(cancellationToken); - - return new RegisterAttendeePayload(attendee); - } - } - } - ``` - -### Add `checkInAttendee` Mutation - -Now that we have the mutation in to register new attendees, let us move on to add another mutation that will allow us to check-in a user to a session. - -1. Add the `CheckInAttendeeInput` to the `Attendees` directory. - - ```csharp - using ConferencePlanner.GraphQL.Data; - using HotChocolate.Types.Relay; - - namespace ConferencePlanner.GraphQL.Attendees - { - public record CheckInAttendeeInput( - [ID(nameof(Session))] - int SessionId, - [ID(nameof(Attendee))] - int AttendeeId); - } - ``` - -1. Next we add the payload type for the `CheckInAttendeePayload` Mutation: - - ```csharp - using System.Threading; - using System.Threading.Tasks; - using ConferencePlanner.GraphQL.Common; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.DataLoader; - - namespace ConferencePlanner.GraphQL.Attendees - { - public class CheckInAttendeePayload : AttendeePayloadBase - { - private int? _sessionId; - - public CheckInAttendeePayload(Attendee attendee, int sessionId) - : base(attendee) - { - _sessionId = sessionId; - } - - public CheckInAttendeePayload(UserError error) - : base(new[] { error }) - { - } - - public async Task GetSessionAsync( - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) - { - if (_sessionId.HasValue) - { - return await sessionById.LoadAsync(_sessionId.Value, cancellationToken); - } - - return null; - } - } - } - ``` - -1. Head back to the `AttendeeMutations` class in the `Attendees` directory and add the `CheckInAttendeeAsync` resolver to it: - - ```csharp - [UseApplicationDbContext] - public async Task CheckInAttendeeAsync( - CheckInAttendeeInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - Attendee attendee = await context.Attendees.FirstOrDefaultAsync( - t => t.Id == input.AttendeeId, cancellationToken); - - if (attendee is null) - { - return new CheckInAttendeePayload( - new UserError("Attendee not found.", "ATTENDEE_NOT_FOUND")); - } - - attendee.SessionsAttendees.Add( - new SessionAttendee - { - SessionId = input.SessionId - }); - - await context.SaveChangesAsync(cancellationToken); - - return new CheckInAttendeePayload(attendee, input.SessionId); - } - ``` - - Your `AttendeeMutations` class should now look like the following: - - ```csharp - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Common; - using ConferencePlanner.GraphQL.Data; - using HotChocolate; - using HotChocolate.Types; - - namespace ConferencePlanner.GraphQL.Attendees - { - [ExtendObjectType(Name = "Mutation")] - public class AttendeeMutations - { - [UseApplicationDbContext] - public async Task RegisterAttendeeAsync( - RegisterAttendeeInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - var attendee = new Attendee - { - FirstName = input.FirstName, - LastName = input.LastName, - UserName = input.UserName, - EmailAddress = input.EmailAddress - }; - - context.Attendees.Add(attendee); - - await context.SaveChangesAsync(cancellationToken); - - return new RegisterAttendeePayload(attendee); - } - - [UseApplicationDbContext] - public async Task CheckInAttendeeAsync( - CheckInAttendeeInput input, - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) - { - Attendee attendee = await context.Attendees.FirstOrDefaultAsync( - t => t.Id == input.AttendeeId, cancellationToken); - - if (attendee is null) - { - return new CheckInAttendeePayload( - new UserError("Attendee not found.", "ATTENDEE_NOT_FOUND")); - } - - attendee.SessionsAttendees.Add( - new SessionAttendee - { - SessionId = input.SessionId - }); - - await context.SaveChangesAsync(cancellationToken); - - return new CheckInAttendeePayload(attendee, input.SessionId); - } - } - } - ``` - -1. Head over to the `Startup.cs` and register the query and mutation type that we have just added with the schema builder. - - ```csharp - services - .AddGraphQLServer() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport() - .AddFiltering() - .AddSorting() - .AddDataLoader() - .AddDataLoader(); - ``` - -1. Start your GraphQL server. - - ```console - dotnet run --project GraphQL - ``` - -1. Validate that you see your new queries and mutations with Banana Cake Pop. - -## Add `onSessionScheduled` Subscription - -With the base in, we now can focus on putting subscriptions on our GraphQL server. GraphQL subscriptions by default work over WebSockets but could also work over SignalR or gRPC. We will first update our request pipeline to use WebSockets, and then we will set up the subscription pub/sub-system. After having our server prepared, we will put in the subscriptions to our API. - -1. Head over to `Startup.cs` and add `app.WebSockets` to the request pipeline. Middleware order is also important with ASP.NET Core, so this middleware needs to come before the GraphQL middleware. - - ```csharp - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseWebSockets(); - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGraphQL(); - }); - } - ``` - -1. Stay in the `Startup.cs` and add `.AddInMemorySubscriptions();` to the `ConfigureServices` method. - - ```csharp - public void ConfigureServices(IServiceCollection services) - { - services.AddPooledDbContextFactory( - options => options.UseSqlite("Data Source=conferences.db")); - - services - .AddGraphQLServer() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport() - .AddFiltering() - .AddSorting() - .AddInMemorySubscriptions() - .AddDataLoader() - .AddDataLoader(); - } - ``` - - > With `app.UseWebSockets()` we have enabled our server to handle websocket request. With `.AddInMemorySubscriptions();` we have added an in-memory pub/sub system for GraphQL subscriptions to our schema. - -1. Add a new class `SessionSubscriptions` to the `Sessions` directory. - - ```csharp - using System.Threading; - using System.Threading.Tasks; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.DataLoader; - using HotChocolate; - using HotChocolate.Types; - - namespace ConferencePlanner.GraphQL.Sessions - { - [ExtendObjectType(Name = "Subscription")] - public class SessionSubscriptions - { - [Subscribe] - [Topic] - public Task OnSessionScheduledAsync( - [EventMessage] int sessionId, - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - sessionById.LoadAsync(sessionId, cancellationToken); - } - } - ``` - - > The `[Topic]` attribute can be put on the method or a parameter of the method and will infer the pub/sub-topic for this subscription. - - > The `[Subscribe]` attribute tells the schema builder that this resolver method needs to be hooked up to the pub/sub-system. This means that in the background, the resolver compiler will create a so-called subscribe resolver that handles subscribing to the pub/sub-system. - - > The `[EventMessage]` attribute marks the parameter where the execution engine shall inject the message payload of the pub/sub-system. - -1. Head back to the `Startup.cs` and register the `SessionSubscriptions` with the schema builder. - - ```csharp - services - .AddGraphQLServer() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddSubscriptionType(d => d.Name("Subscription")) - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport() - .AddFiltering() - .AddSorting() - .AddInMemorySubscriptions() - .AddDataLoader() - .AddDataLoader(); - ``` - - The subscription type itself is now registered, but we still need something to trigger the event. So, next, we are going to update our `scheduleSession` resolver to trigger an event. - -1. Head over to the `SessionMutations` class in the `Sessions` directory and replace `ScheduleSessionAsync` with the following code: - - ```csharp - [UseApplicationDbContext] - public async Task ScheduleSessionAsync( - ScheduleSessionInput input, - [ScopedService] ApplicationDbContext context, - [Service]ITopicEventSender eventSender) - { - if (input.EndTime < input.StartTime) - { - return new ScheduleSessionPayload( - new UserError("endTime has to be larger than startTime.", "END_TIME_INVALID")); - } - - Session session = await context.Sessions.FindAsync(input.SessionId); - - if (session is null) - { - return new ScheduleSessionPayload( - new UserError("Session not found.", "SESSION_NOT_FOUND")); - } - - session.TrackId = input.TrackId; - session.StartTime = input.StartTime; - session.EndTime = input.EndTime; - - await context.SaveChangesAsync(); - - await eventSender.SendAsync( - nameof(SessionSubscriptions.OnSessionScheduledAsync), - session.Id); - - return new ScheduleSessionPayload(session); - } - ``` - - > Our improved resolver now injects `[Service]ITopicEventSender eventSender`. This gives us access to send messages to the underlying pub/sub-system. - - > After `await context.SaveChangesAsync();` we are sending in a new message. - - ```csharp - await eventSender.SendAsync( - nameof(SessionSubscriptions.OnSessionScheduledAsync), - session.Id); - ``` - - > Since we added the `[Topic]` attribute on our resolver method in the `SessionSubscriptions` class, the topic is now the name of this method. A topic can be anything that can be serialized and has equality implemented so you could also use an object. - -1. Start your GraphQL server. - - ```console - dotnet run --project GraphQL - ``` - -1. Open Banana Cake Pop and refresh the schema. - -1. Open a new query tab and add the following subscription query: - - ```graphql - subscription { - onSessionScheduled { - title - startTime - } - } - ``` - - Execute the subscription query. Nothing will happen at this point, and you will just see a loading indicator. - - ![Subscription Waiting for Events](images/31-bcp-subscribe.png) - -1. Open another tab in Banana Cake Pop and add the following query: - - ```graphql - query GetSessionsAndTracks { - sessions { - nodes { - id - } - } - tracks { - nodes { - id - } - } - } - - mutation ScheduleSession { - scheduleSession( - input: { - sessionId: "U2Vzc2lvbgppMQ==" - trackId: "VHJhY2sKaTE=" - startTime: "2020-08-01T16:00" - endTime: "2020-08-01T17:00" - } - ) { - session { - title - } - } - } - ``` - - Execute `GetSessionsAndTracks` first by clicking in the execute link above it. Use the IDs from the response for `ScheduleSession` and execute it once you have filled in the correct IDs. - - ![Subscription Waiting for Events](images/32-bcp-scheduled.png) - -1. Return to your first query tab (the tab where you specified the subscription query). - - ![Subscription Waiting for Events](images/33-bcp-subscription-result.png) - - The event was raised, and our subscription query was executed. We can also see that the loading indicator is still turning since we are still subscribed, and we will get new responses whenever the event is raised. With GraphQL a subscription stream can be infinite or finite. A finite stream will automatically complete whenever the server chooses to complete the topic `ITopicEventSender.CompleteAsync`. - - To stop the subscription from the client-side, click on the stop button right of the loading indicator. - -## Add `onAttendeeCheckedIn` subscription - -The `onSessionScheduled` was quite simple since we did not subscribe to a dynamic topic. Meaning a topic that is defined at the moment we subscribe to it or a topic that depends on the user-context. With `onAttendeeCheckedIn`, we will subscribe to a specific session to see who checked in and how quickly it fills up. - -1. Head over to the `AttendeeMutations` class and replace the `CheckInAttendeeAsync` resolver with the following code: - - ```csharp - [UseApplicationDbContext] - public async Task CheckInAttendeeAsync( - CheckInAttendeeInput input, - [ScopedService] ApplicationDbContext context, - [Service] ITopicEventSender eventSender, - CancellationToken cancellationToken) - { - Attendee attendee = await context.Attendees.FirstOrDefaultAsync( - t => t.Id == input.AttendeeId, cancellationToken); - - if (attendee is null) - { - return new CheckInAttendeePayload( - new UserError("Attendee not found.", "ATTENDEE_NOT_FOUND")); - } - - attendee.SessionsAttendees.Add( - new SessionAttendee - { - SessionId = input.SessionId - }); - - await context.SaveChangesAsync(cancellationToken); - - await eventSender.SendAsync( - "OnAttendeeCheckedIn_" + input.SessionId, - input.AttendeeId, - cancellationToken); - - return new CheckInAttendeePayload(attendee, input.SessionId); - } - ``` - - In this instance, we are again using our `ITopicEventSender` to send messages to our pub/sub-system. However, we are now creating a string topic combined with parts of the input `input.SessionId` and a string describing the event `OnAttendeeCheckedIn_`. If nobody is subscribed, the messages will just be dropped. - - ```csharp - await eventSender.SendAsync( - "OnAttendeeCheckedIn_" + input.SessionId, - input.AttendeeId, - cancellationToken); - ``` - -1. Add a new class `SessionAttendeeCheckIn` to the `Attendees` directory. This will be our subscription payload. - - ```csharp - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.DataLoader; - using HotChocolate; - using HotChocolate.Types.Relay; - - namespace ConferencePlanner.GraphQL.Attendees - { - public class SessionAttendeeCheckIn - { - public SessionAttendeeCheckIn(int attendeeId, int sessionId) - { - AttendeeId = attendeeId; - SessionId = sessionId; - } - - [ID(nameof(Attendee))] - public int AttendeeId { get; } - - [ID(nameof(Session))] - public int SessionId { get; } - - [UseApplicationDbContext] - public async Task CheckInCountAsync( - [ScopedService] ApplicationDbContext context, - CancellationToken cancellationToken) => - await context.Sessions - .Where(session => session.Id == SessionId) - .SelectMany(session => session.SessionAttendees) - .CountAsync(cancellationToken); - - public Task GetAttendeeAsync( - AttendeeByIdDataLoader attendeeById, - CancellationToken cancellationToken) => - attendeeById.LoadAsync(AttendeeId, cancellationToken); - - public Task GetSessionAsync( - SessionByIdDataLoader sessionById, - CancellationToken cancellationToken) => - sessionById.LoadAsync(SessionId, cancellationToken); - } - } - ``` - -1. Create a new class, `AttendeeSubscriptions` and put it in the `Attendees` directory. - - ```csharp - using System.Threading; - using System.Threading.Tasks; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.DataLoader; - using HotChocolate; - using HotChocolate.Execution; - using HotChocolate.Subscriptions; - using HotChocolate.Types; - using HotChocolate.Types.Relay; - - namespace ConferencePlanner.GraphQL.Attendees - { - [ExtendObjectType(Name = "Subscription")] - public class AttendeeSubscriptions - { - [Subscribe(With = nameof(SubscribeToOnAttendeeCheckedInAsync))] - public SessionAttendeeCheckIn OnAttendeeCheckedIn( - [ID(nameof(Session))] int sessionId, - [EventMessage] int attendeeId) => - new SessionAttendeeCheckIn(attendeeId, sessionId); - - public async ValueTask> SubscribeToOnAttendeeCheckedInAsync( - int sessionId, - [Service] ITopicEventReceiver eventReceiver, - CancellationToken cancellationToken) => - await eventReceiver.SubscribeAsync( - "OnAttendeeCheckedIn_" + sessionId, cancellationToken); - } - } - ``` - - `OnAttendeeCheckedIn` represents our resolver like in the first subscription we built, but now in our `SubscribeAttribute` we are referring to a method called `SubscribeToOnAttendeeCheckedInAsync`. So, instead of letting the system generate a subscribe resolver that handles subscribing to the pub/sub-system we are creating it ourselves in order to control how it is done or event order to filter out events that we do not want to pass down. - - ```csharp - public async ValueTask> SubscribeToOnAttendeeCheckedInAsync( - int sessionId, - [Service] ITopicEventReceiver eventReceiver, - CancellationToken cancellationToken) => - await eventReceiver.SubscribeAsync( - "OnAttendeeCheckedIn_" + sessionId, cancellationToken); - ``` - - The subscribe resolver is using `ITopicEventReceiver` to subscribe to a topic. A subscribe resolver can return `IAsyncEnumerable`, `IEnumerable` or `IObservable` to represent the subscription stream. The subscribe resolver has access to all the arguments that the actual resolver has access to. - - 1. Head back to the `Startup.cs` and register this new subscription type with the schema builder. - - ```csharp - services - .AddGraphQLServer() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddTypeExtension() - .AddSubscriptionType(d => d.Name("Subscription")) - .AddTypeExtension() - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport() - .AddFiltering() - .AddSorting() - .AddInMemorySubscriptions() - .AddDataLoader() - .AddDataLoader(); - ``` - - 1. Start your GraphQL server again. - - ```console - dotnet run --project GraphQL - ``` - - 1. Open a new tab and put the following query document in: - - ```graphql - query GetSessions { - sessions { - nodes { - id - } - } - } - - mutation RegisterAttendee { - registerAttendee( - input: { - emailAddress: "michael@chillicream.com" - firstName: "michael" - lastName: "staib" - userName: "michael" - } - ) { - attendee { - id - } - } - } - - mutation CheckInAttendee { - checkInAttendee( - input: { attendeeId: "QXR0ZW5kZWUKaTE=", sessionId: "U2Vzc2lvbgppMQ==" } - ) { - attendee { - userName - } - session { - title - } - } - } - ``` - - Execute `GetSessions` first the resulting session ID and feed it into the `CheckInAttendee` operation. - - ![Execute GetSessions](images/34-bcp-GetSessions.png) - - Next, Execute `RegisterAttendee` take the resulting attendee ID and feed it into the `CheckInAttendee` operation. - - ![Execute RegisterAttendee](images/35-bcp-RegisterAttendee.png) - - 1. Open another tab in Banana Cake Pop and add the following query document: - - ```graphql - subscription OnAttendeeCheckedIn { - onAttendeeCheckedIn(sessionId: "U2Vzc2lvbgppMQ==") { - checkInCount - attendee { - userName - } - } - } - ``` - - Feed-in the session ID you gathered earlier and pass it into the `sessionId` argument of `OnAttendeeCheckedIn`. - - Execute `OnAttendeeCheckedIn`, again nothing will happen at this point, and the query tab is just waiting for incoming messages. - - ![Execute OnAttendeeCheckedIn](images/37-bcp-OnAttendeeCheckedIn.png) - - 1. Get back to the earlier tab and execute the `CheckInAttendee` operation. - - ![Execute CheckInAttendee](images/36-bcp-CheckInAttendee.png) - - 1. Click on the subscription tab to verify that we have received the message that an attendee has checked into our session. - - ![OnAttendeeCheckedIn Received Result](images/38-bcp-OnAttendeeCheckedIn.png) - -## Summary - -In this session, we have learned how we can use GraphQL subscription to provide real-time events. GraphQL makes it easy to work with real-time data since we can specify what data we want to receive when an event happens on our system. - -[**<< Session #6 - Adding complex filter capabilities**](6-adding-complex-filter-capabilities.md) | [**Session #8 - Testing the GraphQL server >>**](8-testing-the-graphql-server.md) diff --git a/docs/7-testing-the-graphql-server.md b/docs/7-testing-the-graphql-server.md new file mode 100644 index 0000000..d1b5deb --- /dev/null +++ b/docs/7-testing-the-graphql-server.md @@ -0,0 +1,178 @@ +# Testing the GraphQL server + +- [Adding a schema change test](#adding-a-schema-change-test) +- [Adding a simple query test](#adding-a-simple-query-test) + +## Adding a schema change test + +A schema change test will simply create a snapshot of your schema, and always fails if the schema changes. This kind of test is often useful when working with pure code-first, where a simple change in C# can create a breaking change in your GraphQL schema. + +1. Create an xUnit test project: + + ```shell + dotnet new xunit3 --name GraphQL.Tests + ``` + +1. Add the project to our solution: + + ```shell + dotnet sln add GraphQL.Tests + ``` + +1. Head over to the `GraphQL.Tests.csproj` file and update the package references to the following: + + ```xml + + + + ``` + +1. Add a reference to the following NuGet packages: + - `CookieCrumble.HotChocolate` version `15.0.3`: + - `dotnet add GraphQL.Tests package CookieCrumble.HotChocolate --version 15.0.3` + - `CookieCrumble.Xunit3` version `15.0.3`: + - `dotnet add GraphQL.Tests package CookieCrumble.Xunit3 --version 15.0.3` + +1. Add a reference to the GraphQL server: + - `dotnet add GraphQL.Tests reference GraphQL` + +1. Rename the file `UnitTest1.cs` to `SchemaTests.cs` and replace the code with the following: + + ```csharp + using ConferencePlanner.GraphQL.Data; + using CookieCrumble; + using Microsoft.Extensions.DependencyInjection; + using HotChocolate.Execution; + + namespace GraphQL.Tests; + + public sealed class SchemaTests + { + [Fact] + public async Task SchemaChanged() + { + // Arrange & act + var schema = await new ServiceCollection() + .AddDbContext() + .AddGraphQLServer() + .AddGlobalObjectIdentification() + .AddMutationConventions() + .AddDbContextCursorPagingProvider() + .AddPagingArguments() + .AddFiltering() + .AddSorting() + .AddInMemorySubscriptions() + .AddGraphQLTypes() + .BuildSchemaAsync(cancellationToken: TestContext.Current.CancellationToken); + + // Assert + schema.MatchSnapshot(extension: ".graphql"); + } + } + ``` + + The above test takes the service collection and builds a schema from it. We call `MatchSnapshot` to create a snapshot of the GraphQL SDL representation of the schema, which is compared in subsequent test runs. + +## Adding a simple query test + +1. Add a reference to the following NuGet packages: + - `Testcontainers.PostgreSql` version `4.2.0`: + - `dotnet add GraphQL.Tests package Testcontainers.PostgreSql --version 4.2.0` + - `Testcontainers.Redis` version `4.2.0`: + - `dotnet add GraphQL.Tests package Testcontainers.Redis --version 4.2.0` + +1. Add a new class named `AttendeeTests.cs`: + + ```csharp + using ConferencePlanner.GraphQL.Data; + using CookieCrumble; + using HotChocolate.Execution; + using Microsoft.EntityFrameworkCore; + using Microsoft.Extensions.DependencyInjection; + using StackExchange.Redis; + using Testcontainers.PostgreSql; + using Testcontainers.Redis; + + namespace GraphQL.Tests; + + public sealed class AttendeeTests : IAsyncLifetime + { + private readonly PostgreSqlContainer _postgreSqlContainer = new PostgreSqlBuilder() + .WithImage("postgres:17.2") + .Build(); + + private readonly RedisContainer _redisContainer = new RedisBuilder() + .WithImage("redis:7.4") + .Build(); + + private IRequestExecutor _requestExecutor = null!; + + public async ValueTask InitializeAsync() + { + // Start test containers. + await Task.WhenAll(_postgreSqlContainer.StartAsync(), _redisContainer.StartAsync()); + + // Build request executor. + _requestExecutor = await new ServiceCollection() + .AddDbContext( + options => options.UseNpgsql(_postgreSqlContainer.GetConnectionString())) + .AddGraphQLServer() + .AddGlobalObjectIdentification() + .AddMutationConventions() + .AddDbContextCursorPagingProvider() + .AddPagingArguments() + .AddFiltering() + .AddSorting() + .AddRedisSubscriptions( + _ => ConnectionMultiplexer.Connect(_redisContainer.GetConnectionString())) + .AddGraphQLTypes() + .BuildRequestExecutorAsync(); + + // Create database. + var dbContext = _requestExecutor.Services + .GetApplicationServices() + .GetRequiredService(); + + await dbContext.Database.EnsureCreatedAsync(); + } + + [Fact] + public async Task RegisterAttendee() + { + // Arrange & act + var result = await _requestExecutor.ExecuteAsync( + """ + mutation RegisterAttendee { + registerAttendee( + input: { + firstName: "Michael" + lastName: "Staib" + username: "michael" + emailAddress: "michael@chillicream.com" + } + ) { + attendee { + id + } + } + } + """, + TestContext.Current.CancellationToken); + + // Assert + result.MatchSnapshot(extension: ".json"); + } + + public async ValueTask DisposeAsync() + { + await _postgreSqlContainer.DisposeAsync(); + await _redisContainer.DisposeAsync(); + } + } + ``` + + In the above test, we use [Testcontainers](https://dotnet.testcontainers.org/) for PostgreSQL and Redis, for realistic integration testing, as opposed to using in-memory providers. + + To execute against a schema we can call `BuildRequestExecutorAsync` on the service collection to get an `IRequestExecutor`. After executing the mutation, we snapshot the result object, and as with the previous test, subsequent test runs will compare our snapshot file. + +[**<< Session #6 - Adding real-time functionality with subscriptions**](6-subscriptions.md) diff --git a/docs/8-testing-the-graphql-server.md b/docs/8-testing-the-graphql-server.md deleted file mode 100644 index 6c36986..0000000 --- a/docs/8-testing-the-graphql-server.md +++ /dev/null @@ -1,145 +0,0 @@ -- [Testing the GraphQL server](#testing-the-graphql-server) - - [Add a schema change test](#add-a-schema-change-test) - - [Add a simple query tests](#add-a-simple-query-tests) - -# Testing the GraphQL server - -There are many ways to test; what we want to have a look at is how we can test parts of the GraphQL schema without writing system tests. - -## Add a schema change test - -A schema change test will simply create a snapshot of your schema and always fails if the schema changes. This kind of test is often useful when working with pure code-first, where a simple change in C# can create a breaking change in your GraphQL schema. - -1. Create a xunit test project. - - ```console - dotnet new xunit -n GraphQL.Tests - ``` - -1. Add the project to our solution. - - ```console - dotnet sln add GraphQL.Tests - ``` - -1. Add a reference to the NuGet package package `Snapshooter.Xunit` version `0.5.7`. - - 1. `dotnet add GraphQL.Tests package Snapshooter.Xunit --version 0.5.7` - -1. Add a reference to the NuGet package package `Microsoft.EntityFrameworkCore.InMemory` version `5.0.0`. - - 1. `dotnet add GraphQL.Tests package Microsoft.EntityFrameworkCore.InMemory --version 5.0.0` - -1. Head over to the `GraphQL.Tests.csproj` and change the version of `xunit` to `2.4.1`. - - ```msbuild - - ``` - -1. Add reference to the GraphQL server. - - 1. `dotnet add GraphQL.Tests reference GraphQL` - -1. Rename the file `UnitTest1.cs` to `AttendeeTests.cs` and replace the code with the following: - - ```csharp - using System; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using Microsoft.Extensions.DependencyInjection; - using ConferencePlanner.GraphQL; - using ConferencePlanner.GraphQL.Attendees; - using ConferencePlanner.GraphQL.Data; - using ConferencePlanner.GraphQL.Sessions; - using ConferencePlanner.GraphQL.Speakers; - using ConferencePlanner.GraphQL.Tracks; - using ConferencePlanner.GraphQL.Types; - using HotChocolate; - using HotChocolate.Execution; - using Snapshooter.Xunit; - using Xunit; - - namespace GraphQL.Tests - { - public class AttendeeTests - { - [Fact] - public async Task Attendee_Schema_Changed() - { - // arrange - // act - ISchema schema = await new ServiceCollection() - .AddPooledDbContextFactory( - options => options.UseInMemoryDatabase("Data Source=conferences.db")) - .AddGraphQL() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport() - .BuildSchemaAsync(); - - // assert - schema.Print().MatchSnapshot(); - } - } - } - ``` - - The above test takes the service collection and builds from it a schema. We only integrates the part needed that we want to snapshot. On the schema, we are doing a `Print` that will print out the GraphQL SDL representation of the schema on which we do a `MatchSnapshot` that will create in the first run a snapshot file and will compare the SDL in consecutive runs against the snapshot file. - -## Add a simple query tests - -1. Add the following test to the AttendeeTests.cs: - - ```csharp - [Fact] - public async Task RegisterAttendee() - { - // arrange - IRequestExecutor executor = await new ServiceCollection() - .AddPooledDbContextFactory( - options => options.UseInMemoryDatabase("Data Source=conferences.db")) - .AddGraphQL() - .AddQueryType(d => d.Name("Query")) - .AddTypeExtension() - .AddMutationType(d => d.Name("Mutation")) - .AddTypeExtension() - .AddType() - .AddType() - .AddType() - .AddType() - .EnableRelaySupport() - .BuildRequestExecutorAsync(); - - // act - IExecutionResult result = await executor.ExecuteAsync(@" - mutation RegisterAttendee { - registerAttendee( - input: { - emailAddress: ""michael@chillicream.com"" - firstName: ""michael"" - lastName: ""staib"" - userName: ""michael3"" - }) - { - attendee { - id - } - } - }"); - - // assert - result.ToJson().MatchSnapshot(); - } - ``` - - In the above test, we again only take the parts of the schema builder that we are concerned about within our test. Also, we have replaced the services that we do not need at this point. - - To execute against a schema we can call `BuildRequestExecutorAsync` on the service collection and get an `IRequestExecutor` to execute queries against our schema. Finally, we snapshot on the result object, and like in the above test, consecutive tests will be validated against our snapshot file. - -[**<< Session #7 - Adding real-time functionality with subscriptions**](7-subscriptions.md) \ No newline at end of file diff --git a/docs/diagrams/17-middleware-flow.md b/docs/diagrams/17-middleware-flow.md new file mode 100644 index 0000000..fc9322b --- /dev/null +++ b/docs/diagrams/17-middleware-flow.md @@ -0,0 +1,11 @@ +# Middleware flow + +```mermaid +sequenceDiagram + Middleware1->>Middleware2: next() + Middleware2->>Middleware3: next() +``` + +--- + +[Mermaid Live Editor](https://mermaid.live/edit#pako:eNpVjzFPwzAQhf9K9CaQTIWdNHE8dGLtxIa8WPHRRq3t4tqiJcp_xxQF1O0-6Xt39yYMwRIUzvSRyQ_0MppdNE77qtqO1h7p00TiT5vNPwlVebqkh8d7SdxJ9SKBwVF0ZrTlyvQT0Uh7cqShymhNPGhoPxfP5BRer36ASjETQwx5t4d6N8dzoXyyJi3_LQrZMYW4_e1wq8JwMv4tBPcXLAw14QLVrFe8EbLrG9k-y7qXDcMVqpMr3nW87oUUQrTrdmb4um3g8zfkh1w0) (export SVG) diff --git a/docs/diagrams/18-middleware-flow.md b/docs/diagrams/18-middleware-flow.md new file mode 100644 index 0000000..5e7c60d --- /dev/null +++ b/docs/diagrams/18-middleware-flow.md @@ -0,0 +1,11 @@ +# Middleware flow + +```mermaid +sequenceDiagram + Middleware1->>Middleware2: next() + Middleware2->>Resolver: next() +``` + +--- + +[Mermaid Live Editor](https://mermaid.live/edit#pako:eNpVjzFPwzAQhf9K9CaQTIXdNHE8dOraBTbkxYqPNmpit44NLVH-O6aoVGz3nT69uzeh9ZagMNIpkWtp05ldMIN2RbHtrO3p0wTiT-v1nYQqHJ3jw-N_SWTphUbff1C4GWAYKAyms_nE9ONrxD0NpKHyaE04aGg3Z8-k6F8vroWKIRFD8Gm3h3o3_ZgpHa2Jt-f-tmS76MP2t8G1CMPRuDfv705mqAlnqHK14KWQdVPK6lkuG1kyXKBqueB1zZeNkEKIalXNDF_XBD5_AzdyW6A) (export SVG) diff --git a/docs/diagrams/19-middleware-flow.md b/docs/diagrams/19-middleware-flow.md new file mode 100644 index 0000000..7e05456 --- /dev/null +++ b/docs/diagrams/19-middleware-flow.md @@ -0,0 +1,11 @@ +# Middleware flow + +```mermaid +sequenceDiagram + UseUpperCase->>Resolver: next() + Resolver->>UseUpperCase: result = result.ToUpperInvariant() +``` + +--- + +[Mermaid Live Editor](https://mermaid.live/edit#pako:eNpNTz1PwzAQ_SvWTVQyEXHTxLHULrAwsABdUJZTcrQRiR38UbVE-e-4gVK2ex_37t4ItWkIFDj6DKRremhxZ7GvNGNbR9thIHuPjm43m2dypjuQVUzT0d8szpYLF-X_bsUsudB5tv4dklczi4_6gLZFHdeBQ0-2x7aJ18dzWAV-Tz1VoOLYoP2ooNJT9GHw5uWka1DeBuJgTdjtQb1j5yIKQ4P-8vcfS03rjX36KTd35DCgfjPm6okY1AhHUNkqSTMhizKT-Z1cljLjcAJVyCQtinRZCimEyFf5xOFrTkinb4cBZnI) (export SVG) diff --git a/docs/diagrams/20-middleware-flow.md b/docs/diagrams/20-middleware-flow.md new file mode 100644 index 0000000..11da200 --- /dev/null +++ b/docs/diagrams/20-middleware-flow.md @@ -0,0 +1,19 @@ +# Middleware flow + +```mermaid +sequenceDiagram + autonumber + UsePaging->>+UseProjection: next(context) + UseProjection->>+UseFiltering: next(context) + UseFiltering->>+UseSorting: next(context) + UseSorting->>+Resolver: next(context) + + Resolver-->>-UseSorting: apply sorting + UseSorting-->>-UseFiltering: apply filtering + UseFiltering-->>-UseProjection: apply projections + UseProjection-->>-UsePaging: apply paging +``` + +--- + +[Mermaid Live Editor](https://mermaid.live/edit#pako:eNp9kstOg0AUhl-FnJVG2gilXGbRlXFnYmzcGDYjnFIUZnAupkh4d4drSVplM_wz33cyOWcaSHiKQEDil0aW4ENOM0HLmFnmo1pxpst3FEN-lfhMs5xlq93urguCf2Cics6IxfCkbhLOlFlvz_hMjMpjXigUpsRfxgyMwp4L9Q8-HnfwC0pefKO4QAd4Ol4ZdrUsTKuqqC05xIvCI72498Afpo0rNx-dZXsGqZp35LUWTV7f49npE9hQoihpnpphNZ0cgzpiiTEQ85tS8RlDzFrDdVPb1ywBooRGGwTX2RHIgRbSJF2lVE1jnhBMc8XF0_AU-hdhQ0XZG-flLJoMpIETEG-7djw3DCIv9O_DTRR6NtRAgnDtBIGzidzQdV1_67c2_PQVnPYXPefPyg) (export SVG) diff --git a/docs/diagrams/22-pagination.md b/docs/diagrams/22-pagination.md new file mode 100644 index 0000000..0f4e697 --- /dev/null +++ b/docs/diagrams/22-pagination.md @@ -0,0 +1,12 @@ +# Pagination + +```mermaid +sequenceDiagram + autonumber + UsePaging->>Resolver: next(context) + Resolver-->>UsePaging: Apply pagination to IQueryable +``` + +--- + +[Mermaid Live Editor](https://mermaid.live/edit#pako:eNpFj01rwzAMhv-K0WmDtCxpmjg-FAa77DDYB7uMXNRYS8Pijzn2aBby3-d0tD1Jj_TqRe8EjZEEAgb6DqQbeuiwdahqzRgGb3RQe3ILvQ_0jG2n29Vu90qD6X_ICabp6G8ao32st4vsvFpF2eVEsHtr-5HZhdB3RjNv2ONLIDfividIQJFT2Mn4ybTY1OAPpKgGEVuJ7quGWs9Rtzz1NuoGhHeBEnAmtAcQn9gPkYKV6M8ZLlOSnTfu6T_oKW8CFvWHMVdNZBATHEHk23WaZ7yscl7c8U3F8wRGECVfp2WZbqqMZ1lWbIs5gd-TQzr_AWxUaz0) (export SVG) diff --git a/docs/images/1-start-server.png b/docs/images/1-start-server.png deleted file mode 100644 index 0854478..0000000 Binary files a/docs/images/1-start-server.png and /dev/null differ diff --git a/docs/images/1-start-server.webp b/docs/images/1-start-server.webp new file mode 100644 index 0000000..19c1c15 Binary files /dev/null and b/docs/images/1-start-server.webp differ diff --git a/docs/images/10-bcp-schema-updated.png b/docs/images/10-bcp-schema-updated.png deleted file mode 100644 index 36b6842..0000000 Binary files a/docs/images/10-bcp-schema-updated.png and /dev/null differ diff --git a/docs/images/10-bcp-schema-updated.webp b/docs/images/10-bcp-schema-updated.webp new file mode 100644 index 0000000..c80a7a7 Binary files /dev/null and b/docs/images/10-bcp-schema-updated.webp differ diff --git a/docs/images/11-bcp-schema-updated.png b/docs/images/11-bcp-schema-updated.png deleted file mode 100644 index 745784a..0000000 Binary files a/docs/images/11-bcp-schema-updated.png and /dev/null differ diff --git a/docs/images/11-bcp-schema-updated.webp b/docs/images/11-bcp-schema-updated.webp new file mode 100644 index 0000000..4daad84 Binary files /dev/null and b/docs/images/11-bcp-schema-updated.webp differ diff --git a/docs/images/12-bcp-speaker-query.png b/docs/images/12-bcp-speaker-query.png deleted file mode 100644 index 6026b40..0000000 Binary files a/docs/images/12-bcp-speaker-query.png and /dev/null differ diff --git a/docs/images/12-bcp-speaker-query.webp b/docs/images/12-bcp-speaker-query.webp new file mode 100644 index 0000000..fb2354d Binary files /dev/null and b/docs/images/12-bcp-speaker-query.webp differ diff --git a/docs/images/13-bcp-node-field.png b/docs/images/13-bcp-node-field.png deleted file mode 100644 index 36a1aa0..0000000 Binary files a/docs/images/13-bcp-node-field.png and /dev/null differ diff --git a/docs/images/13-bcp-node-field.webp b/docs/images/13-bcp-node-field.webp new file mode 100644 index 0000000..2311683 Binary files /dev/null and b/docs/images/13-bcp-node-field.webp differ diff --git a/docs/images/14-bcp-speaker-ids.png b/docs/images/14-bcp-speaker-ids.png deleted file mode 100644 index 2b02c5b..0000000 Binary files a/docs/images/14-bcp-speaker-ids.png and /dev/null differ diff --git a/docs/images/15-bcp-speaker-node-field.png b/docs/images/15-bcp-speaker-node-field.png deleted file mode 100644 index 26e5e22..0000000 Binary files a/docs/images/15-bcp-speaker-node-field.png and /dev/null differ diff --git a/docs/images/16-bcp-speaker-with-node-id.png b/docs/images/16-bcp-speaker-with-node-id.png deleted file mode 100644 index 9818713..0000000 Binary files a/docs/images/16-bcp-speaker-with-node-id.png and /dev/null differ diff --git a/docs/images/17-middleware-flow.png b/docs/images/17-middleware-flow.png deleted file mode 100644 index 2764154..0000000 Binary files a/docs/images/17-middleware-flow.png and /dev/null differ diff --git a/docs/images/17-middleware-flow.svg b/docs/images/17-middleware-flow.svg new file mode 100644 index 0000000..227b770 --- /dev/null +++ b/docs/images/17-middleware-flow.svg @@ -0,0 +1,3 @@ + + +Middleware3Middleware2Middleware1Middleware3Middleware2Middleware1next()next() \ No newline at end of file diff --git a/docs/images/18-middleware-flow.png b/docs/images/18-middleware-flow.png deleted file mode 100644 index e2eff1e..0000000 Binary files a/docs/images/18-middleware-flow.png and /dev/null differ diff --git a/docs/images/18-middleware-flow.svg b/docs/images/18-middleware-flow.svg new file mode 100644 index 0000000..801b0a2 --- /dev/null +++ b/docs/images/18-middleware-flow.svg @@ -0,0 +1,3 @@ + + +ResolverMiddleware2Middleware1ResolverMiddleware2Middleware1next()next() \ No newline at end of file diff --git a/docs/images/19-middleware-flow.png b/docs/images/19-middleware-flow.png deleted file mode 100644 index 3154a29..0000000 Binary files a/docs/images/19-middleware-flow.png and /dev/null differ diff --git a/docs/images/19-middleware-flow.svg b/docs/images/19-middleware-flow.svg new file mode 100644 index 0000000..5e57197 --- /dev/null +++ b/docs/images/19-middleware-flow.svg @@ -0,0 +1,3 @@ + + +ResolverUseUpperCaseResolverUseUpperCasenext()result = result.ToUpperInvariant() \ No newline at end of file diff --git a/docs/images/2-bcp-connect-to-server.png b/docs/images/2-bcp-connect-to-server.png deleted file mode 100644 index 0944ff9..0000000 Binary files a/docs/images/2-bcp-connect-to-server.png and /dev/null differ diff --git a/docs/images/2-bcp-connect-to-server.webp b/docs/images/2-bcp-connect-to-server.webp new file mode 100644 index 0000000..05cfab4 Binary files /dev/null and b/docs/images/2-bcp-connect-to-server.webp differ diff --git a/docs/images/20-middleware-flow.png b/docs/images/20-middleware-flow.png deleted file mode 100644 index 7c98fcc..0000000 Binary files a/docs/images/20-middleware-flow.png and /dev/null differ diff --git a/docs/images/20-middleware-flow.svg b/docs/images/20-middleware-flow.svg new file mode 100644 index 0000000..8b2a37c --- /dev/null +++ b/docs/images/20-middleware-flow.svg @@ -0,0 +1,3 @@ + + +ResolverUseSortingUseFilteringUseProjectionUsePagingResolverUseSortingUseFilteringUseProjectionUsePagingnext(context)1next(context)2next(context)3next(context)4apply sorting5apply filtering6apply projections7apply paging8 \ No newline at end of file diff --git a/docs/images/21-conference-planner-db-diagram.png b/docs/images/21-conference-planner-db-diagram.png deleted file mode 100644 index 75aa2e7..0000000 Binary files a/docs/images/21-conference-planner-db-diagram.png and /dev/null differ diff --git a/docs/images/21-conference-planner-db-diagram.webp b/docs/images/21-conference-planner-db-diagram.webp new file mode 100644 index 0000000..41e95f0 Binary files /dev/null and b/docs/images/21-conference-planner-db-diagram.webp differ diff --git a/docs/images/22-pagination.png b/docs/images/22-pagination.png deleted file mode 100644 index 998faf2..0000000 Binary files a/docs/images/22-pagination.png and /dev/null differ diff --git a/docs/images/22-pagination.svg b/docs/images/22-pagination.svg new file mode 100644 index 0000000..17f956c --- /dev/null +++ b/docs/images/22-pagination.svg @@ -0,0 +1,3 @@ + + +ResolverUsePagingResolverUsePagingnext(context)1Apply pagination to IQueryable2 \ No newline at end of file diff --git a/docs/images/23-bcp-schema.png b/docs/images/23-bcp-schema.png deleted file mode 100644 index c244fd5..0000000 Binary files a/docs/images/23-bcp-schema.png and /dev/null differ diff --git a/docs/images/24-bcp-schema.png b/docs/images/24-bcp-schema.png deleted file mode 100644 index 43ce299..0000000 Binary files a/docs/images/24-bcp-schema.png and /dev/null differ diff --git a/docs/images/24-bcp-schema.webp b/docs/images/24-bcp-schema.webp new file mode 100644 index 0000000..9a7a297 Binary files /dev/null and b/docs/images/24-bcp-schema.webp differ diff --git a/docs/images/25-bcp-GetFirstTrack.png b/docs/images/25-bcp-GetFirstTrack.png deleted file mode 100644 index 86196c4..0000000 Binary files a/docs/images/25-bcp-GetFirstTrack.png and /dev/null differ diff --git a/docs/images/25-bcp-get-first-track.webp b/docs/images/25-bcp-get-first-track.webp new file mode 100644 index 0000000..a78d60b Binary files /dev/null and b/docs/images/25-bcp-get-first-track.webp differ diff --git a/docs/images/26-bcp-GetNextTrack.png b/docs/images/26-bcp-GetNextTrack.png deleted file mode 100644 index 0c05146..0000000 Binary files a/docs/images/26-bcp-GetNextTrack.png and /dev/null differ diff --git a/docs/images/26-bcp-get-next-track.webp b/docs/images/26-bcp-get-next-track.webp new file mode 100644 index 0000000..abc0ef4 Binary files /dev/null and b/docs/images/26-bcp-get-next-track.webp differ diff --git a/docs/images/27-bcp-schema.png b/docs/images/27-bcp-schema.png deleted file mode 100644 index b4e3ad2..0000000 Binary files a/docs/images/27-bcp-schema.png and /dev/null differ diff --git a/docs/images/27-bcp-schema.webp b/docs/images/27-bcp-schema.webp new file mode 100644 index 0000000..ab41369 Binary files /dev/null and b/docs/images/27-bcp-schema.webp differ diff --git a/docs/images/28-bcp-GetTrackWithSessions.png b/docs/images/28-bcp-GetTrackWithSessions.png deleted file mode 100644 index 9f6ce9f..0000000 Binary files a/docs/images/28-bcp-GetTrackWithSessions.png and /dev/null differ diff --git a/docs/images/28-bcp-get-track-with-sessions.webp b/docs/images/28-bcp-get-track-with-sessions.webp new file mode 100644 index 0000000..62d7dee Binary files /dev/null and b/docs/images/28-bcp-get-track-with-sessions.webp differ diff --git a/docs/images/29-bcp-filter-type.png b/docs/images/29-bcp-filter-type.png deleted file mode 100644 index 4fbc328..0000000 Binary files a/docs/images/29-bcp-filter-type.png and /dev/null differ diff --git a/docs/images/29-bcp-filter-type.webp b/docs/images/29-bcp-filter-type.webp new file mode 100644 index 0000000..76c9044 Binary files /dev/null and b/docs/images/29-bcp-filter-type.webp differ diff --git a/docs/images/3-bcp-browse-schema.webp b/docs/images/3-bcp-browse-schema.webp new file mode 100644 index 0000000..4e37eda Binary files /dev/null and b/docs/images/3-bcp-browse-schema.webp differ diff --git a/docs/images/3-bcp-schema-explorer.png b/docs/images/3-bcp-schema-explorer.png deleted file mode 100644 index 03b2c65..0000000 Binary files a/docs/images/3-bcp-schema-explorer.png and /dev/null differ diff --git a/docs/images/30-bcp-get-session2.webp b/docs/images/30-bcp-get-session2.webp new file mode 100644 index 0000000..c02c172 Binary files /dev/null and b/docs/images/30-bcp-get-session2.webp differ diff --git a/docs/images/30-bcp-get-sessions.png b/docs/images/30-bcp-get-sessions.png deleted file mode 100644 index 4a5e5bd..0000000 Binary files a/docs/images/30-bcp-get-sessions.png and /dev/null differ diff --git a/docs/images/31-bcp-subscribe.png b/docs/images/31-bcp-subscribe.png deleted file mode 100644 index 534e3e3..0000000 Binary files a/docs/images/31-bcp-subscribe.png and /dev/null differ diff --git a/docs/images/31-bcp-subscribe.webp b/docs/images/31-bcp-subscribe.webp new file mode 100644 index 0000000..a16cf53 Binary files /dev/null and b/docs/images/31-bcp-subscribe.webp differ diff --git a/docs/images/32-bcp-scheduled.png b/docs/images/32-bcp-scheduled.png deleted file mode 100644 index f4bc4cc..0000000 Binary files a/docs/images/32-bcp-scheduled.png and /dev/null differ diff --git a/docs/images/32-bcp-scheduled.webp b/docs/images/32-bcp-scheduled.webp new file mode 100644 index 0000000..50c4c1d Binary files /dev/null and b/docs/images/32-bcp-scheduled.webp differ diff --git a/docs/images/33-bcp-subscription-result.png b/docs/images/33-bcp-subscription-result.png deleted file mode 100644 index eb251be..0000000 Binary files a/docs/images/33-bcp-subscription-result.png and /dev/null differ diff --git a/docs/images/33-bcp-subscription-result.webp b/docs/images/33-bcp-subscription-result.webp new file mode 100644 index 0000000..8243f44 Binary files /dev/null and b/docs/images/33-bcp-subscription-result.webp differ diff --git a/docs/images/34-bcp-GetSessions.png b/docs/images/34-bcp-GetSessions.png deleted file mode 100644 index e915077..0000000 Binary files a/docs/images/34-bcp-GetSessions.png and /dev/null differ diff --git a/docs/images/34-bcp-get-sessions.webp b/docs/images/34-bcp-get-sessions.webp new file mode 100644 index 0000000..b13f3b2 Binary files /dev/null and b/docs/images/34-bcp-get-sessions.webp differ diff --git a/docs/images/35-bcp-RegisterAttendee.png b/docs/images/35-bcp-RegisterAttendee.png deleted file mode 100644 index cfe7226..0000000 Binary files a/docs/images/35-bcp-RegisterAttendee.png and /dev/null differ diff --git a/docs/images/35-bcp-register-attendee.webp b/docs/images/35-bcp-register-attendee.webp new file mode 100644 index 0000000..2ffbd7d Binary files /dev/null and b/docs/images/35-bcp-register-attendee.webp differ diff --git a/docs/images/36-bcp-CheckInAttendee.png b/docs/images/36-bcp-CheckInAttendee.png deleted file mode 100644 index 63b56a5..0000000 Binary files a/docs/images/36-bcp-CheckInAttendee.png and /dev/null differ diff --git a/docs/images/36-bcp-on-attendee-checked-in.webp b/docs/images/36-bcp-on-attendee-checked-in.webp new file mode 100644 index 0000000..93c9cb5 Binary files /dev/null and b/docs/images/36-bcp-on-attendee-checked-in.webp differ diff --git a/docs/images/37-bcp-OnAttendeeCheckedIn.png b/docs/images/37-bcp-OnAttendeeCheckedIn.png deleted file mode 100644 index d423463..0000000 Binary files a/docs/images/37-bcp-OnAttendeeCheckedIn.png and /dev/null differ diff --git a/docs/images/37-bcp-check-in-attendee.webp b/docs/images/37-bcp-check-in-attendee.webp new file mode 100644 index 0000000..1264e8d Binary files /dev/null and b/docs/images/37-bcp-check-in-attendee.webp differ diff --git a/docs/images/38-bcp-OnAttendeeCheckedIn.png b/docs/images/38-bcp-OnAttendeeCheckedIn.png deleted file mode 100644 index 8779e39..0000000 Binary files a/docs/images/38-bcp-OnAttendeeCheckedIn.png and /dev/null differ diff --git a/docs/images/38-bcp-on-attendee-checked-in.webp b/docs/images/38-bcp-on-attendee-checked-in.webp new file mode 100644 index 0000000..2eca647 Binary files /dev/null and b/docs/images/38-bcp-on-attendee-checked-in.webp differ diff --git a/docs/images/39-bcp-verify-nullability.png b/docs/images/39-bcp-verify-nullability.png deleted file mode 100644 index db750e2..0000000 Binary files a/docs/images/39-bcp-verify-nullability.png and /dev/null differ diff --git a/docs/images/4-bcp-browse-schema-mutation.webp b/docs/images/4-bcp-browse-schema-mutation.webp new file mode 100644 index 0000000..25fb0ba Binary files /dev/null and b/docs/images/4-bcp-browse-schema-mutation.webp differ diff --git a/docs/images/4-bcp-schema-explorer-mutation.png b/docs/images/4-bcp-schema-explorer-mutation.png deleted file mode 100644 index b5b70e2..0000000 Binary files a/docs/images/4-bcp-schema-explorer-mutation.png and /dev/null differ diff --git a/docs/images/5-bcp-mutation-add-addspeaker.png b/docs/images/5-bcp-mutation-add-addspeaker.png deleted file mode 100644 index b27c7e7..0000000 Binary files a/docs/images/5-bcp-mutation-add-addspeaker.png and /dev/null differ diff --git a/docs/images/5-bcp-mutation-add-speaker.webp b/docs/images/5-bcp-mutation-add-speaker.webp new file mode 100644 index 0000000..fff47d3 Binary files /dev/null and b/docs/images/5-bcp-mutation-add-speaker.webp differ diff --git a/docs/images/6-bcp-query-get-speakers.png b/docs/images/6-bcp-query-get-speakers.png deleted file mode 100644 index e7756ef..0000000 Binary files a/docs/images/6-bcp-query-get-speakers.png and /dev/null differ diff --git a/docs/images/6-bcp-query-get-speakers.webp b/docs/images/6-bcp-query-get-speakers.webp new file mode 100644 index 0000000..e51fe34 Binary files /dev/null and b/docs/images/6-bcp-query-get-speakers.webp differ diff --git a/docs/images/7-vscode-nullability-warnings.png b/docs/images/7-vscode-nullability-warnings.png deleted file mode 100644 index 0c85443..0000000 Binary files a/docs/images/7-vscode-nullability-warnings.png and /dev/null differ diff --git a/docs/images/8-bcp-dbcontext-error.png b/docs/images/8-bcp-dbcontext-error.png deleted file mode 100644 index 7047d8e..0000000 Binary files a/docs/images/8-bcp-dbcontext-error.png and /dev/null differ diff --git a/docs/images/9-bcp-dbcontext-works-inparallel.png b/docs/images/9-bcp-dbcontext-works-inparallel.png deleted file mode 100644 index 087d644..0000000 Binary files a/docs/images/9-bcp-dbcontext-works-inparallel.png and /dev/null differ diff --git a/docs/images/ChilliCream.svg b/docs/images/ChilliCream.svg deleted file mode 100644 index 6db39cc..0000000 --- a/docs/images/ChilliCream.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file