diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee2ccd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,275 @@ + +# Created by https://www.gitignore.io/api/aspnetcore + +### ASPNETCore ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 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 + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_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 ignoreable 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 +node_modules/ +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 + +# 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 + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/ + + +# End of https://www.gitignore.io/api/aspnetcore \ No newline at end of file diff --git a/Building a RESTful API with ASP.NET Core 3.postman_collection b/Building a RESTful API with ASP.NET Core 3.postman_collection new file mode 100644 index 0000000..8714033 --- /dev/null +++ b/Building a RESTful API with ASP.NET Core 3.postman_collection @@ -0,0 +1,1562 @@ +{ + "info": { + "_postman_id": "f5e567e0-9d69-4e19-bae3-23fc1978a395", + "name": "REST course", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "GET Authors", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:51044/api/authors", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors" + ] + } + }, + "response": [] + }, + { + "name": "GET Author", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/vnd.marvin.hateoas+json", + "type": "text" + } + ], + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35" + ] + } + }, + "response": [] + }, + { + "name": "GET Author (unexisting)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:51044/api/authors/a8d15573-ec65-4f48-97d2-2e7c0a726c33", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "a8d15573-ec65-4f48-97d2-2e7c0a726c33" + ] + } + }, + "response": [] + }, + { + "name": "GET Author (Accept: application/json)", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35" + ] + } + }, + "response": [] + }, + { + "name": "GET Author (Accept: application/xml)", + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/xml" + } + ], + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35" + ] + } + }, + "response": [] + }, + { + "name": "GET Courses for Author", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses" + ] + } + }, + "response": [] + }, + { + "name": "GET Courses for Author (unexisting Author)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:51044/api/authors/b29e03b5-ba28-4489-8834-689de28af370/courses", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "b29e03b5-ba28-4489-8834-689de28af370", + "courses" + ] + } + }, + "response": [] + }, + { + "name": "GET Course for Author", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "5b1c2b4d-48c7-402a-80c3-cc796ad49c6b" + ] + } + }, + "response": [] + }, + { + "name": "GET Course for Author (unexisting Author)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:51044/api/authors/b29e03b5-ba28-4489-8834-689de28af370/courses/bc4c35c3-3857-4250-9449-155fcf5109ec", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "b29e03b5-ba28-4489-8834-689de28af370", + "courses", + "bc4c35c3-3857-4250-9449-155fcf5109ec" + ] + } + }, + "response": [] + }, + { + "name": "GET Course for Author (unexisting Course)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/8afc4f43-3d02-429b-90c7-1cabe201bf7a", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "8afc4f43-3d02-429b-90c7-1cabe201bf7a" + ] + } + }, + "response": [] + }, + { + "name": "HEAD Authors", + "request": { + "method": "HEAD", + "header": [], + "url": { + "raw": "http://localhost:51044/api/authors", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors" + ] + } + }, + "response": [] + }, + { + "name": "GET Filtered Authors", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:51044/api/authors?mainCategory=Rum", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors" + ], + "query": [ + { + "key": "mainCategory", + "value": "Rum" + } + ] + } + }, + "response": [] + }, + { + "name": "GET Searched Authors", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:51044/api/authors?searchQuery=a", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors" + ], + "query": [ + { + "key": "searchQuery", + "value": "a" + } + ] + } + }, + "response": [] + }, + { + "name": "GET Filtered and Searched Authors", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:51044/api/authors?mainCategory=Rum&searchQuery=a", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors" + ], + "query": [ + { + "key": "mainCategory", + "value": "Rum" + }, + { + "key": "searchQuery", + "value": "a" + } + ] + } + }, + "response": [] + }, + { + "name": "POST Author", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"firstName\" : \"Jane\",\n\t\"lastName\" : \"Skewers\",\n\t\"dateOfBirth\" : \"1968-03-04T00:00:00\",\n\t\"mainCategory\": \"Rum\"\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors" + ] + } + }, + "response": [] + }, + { + "name": "POST Author (no body)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "http://localhost:51044/api/authors", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors" + ] + } + }, + "response": [] + }, + { + "name": "POST Author (invalid body)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"firstName\" : \"Jane\",\n\t\"lastName\" : \"Skewers\",\n\t\"dateOfBirth\" : \"invalid value for DateTimeOffset\",\n\t\"mainCategory\": \"Rum\"\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors" + ] + } + }, + "response": [] + }, + { + "name": "POST Course for Author", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"title\" : \"Top Pirate Hits of Last Decade\",\n\t\"description\" : \"Learn the lyrics and notes to the latest pirate hits\"\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors/2902b665-1190-4c70-9915-b9c2d7680450/courses", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "2902b665-1190-4c70-9915-b9c2d7680450", + "courses" + ] + } + }, + "response": [] + }, + { + "name": "POST Course for Author (unexisting Author)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"title\" : \"Top Pirate Hits of Last Decade\",\n\t\"description\" : \"Learn the lyrics and notes to the latest pirate hits\"\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors/0d75ab75-0028-40c3-8019-1188fe7e790a/courses", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "0d75ab75-0028-40c3-8019-1188fe7e790a", + "courses" + ] + } + }, + "response": [] + }, + { + "name": "POST Author with Courses", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"firstName\" : \"Jane\",\n\t\"lastName\" : \"Skewers\",\n\t\"dateOfBirth\" : \"1968-03-04T00:00:00\",\n\t\"mainCategory\": \"Rum\",\n\t\"courses\": [\n\t\t{\n\t\t\t\"title\" : \"Drinking Games for Lazy Pirates\",\n\t\t\t\"description\" : \"The best drinking games for pirates that don't like to move their feet unless strictly necessary.\"\n\t\t},\n\t\t{\n\t\t\t\"title\" : \"Rum Degustation 101\",\n\t\t\t\"description\" : \"Learn all about rum degustation, from differences in color, taste and chance of debauchery\"\n\t\t}\n\t\t]\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors" + ] + } + }, + "response": [] + }, + { + "name": "POST Author collection", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "[{\n\t\"firstName\" : \"Jane\",\n\t\"lastName\" : \"Skewers\",\n\t\"dateOfBirth\" : \"1968-03-04T00:00:00\",\n\t\"mainCategory\": \"Rum\"\n},\n{\n\t\"firstName\" : \"Jack\",\n\t\"lastName\" : \"Pepper\",\n\t\"dateOfBirth\" : \"1981-05-03T00:00:00\",\n\t\"mainCategory\": \"Singing\"\n}]" + }, + "url": { + "raw": "http://localhost:51044/api/authorcollections", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authorcollections" + ] + } + }, + "response": [] + }, + { + "name": "POST Author to single resource URI", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"firstName\" : \"Jane\",\n\t\"lastName\" : \"Skewers\",\n\t\"dateOfBirth\" : \"1968-03-04T00:00:00\",\n\t\"mainCategory\": \"Rum\"\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors/25141d83-4584-4487-a306-0441695d8e24", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "25141d83-4584-4487-a306-0441695d8e24" + ] + } + }, + "response": [] + }, + { + "name": "OPTIONS Authors", + "request": { + "method": "OPTIONS", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "http://localhost:51044/api/authors", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors" + ] + } + }, + "response": [] + }, + { + "name": "POST Author (XML input)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/xml" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "\r\n \r\n 1968-03-03T00:00:00Z\r\n 0\r\n \r\n Jane \r\n Skewers\r\n Rum\r\n" + }, + "url": { + "raw": "http://localhost:51044/api/authors", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors" + ] + } + }, + "response": [] + }, + { + "name": "POST Author (XML input, XML output)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/xml" + }, + { + "key": "Accept", + "value": "application/xml" + } + ], + "body": { + "mode": "raw", + "raw": "\r\n \r\n 1968-03-03T00:00:00Z\r\n 0\r\n \r\n Jane \r\n Skewers\r\n Rum\r\n\r\n" + }, + "url": { + "raw": "http://localhost:51044/api/authors", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors" + ] + } + }, + "response": [] + }, + { + "name": "POST Course for Author (null values)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"title\" : null,\n\t\"description\" : null\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors/2902b665-1190-4c70-9915-b9c2d7680450/courses", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "2902b665-1190-4c70-9915-b9c2d7680450", + "courses" + ] + } + }, + "response": [] + }, + { + "name": "POST Course for Author (title == description)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\" : \"A new course\",\n \"description\" : \"A new course\"\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors/2902b665-1190-4c70-9915-b9c2d7680450/courses", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "2902b665-1190-4c70-9915-b9c2d7680450", + "courses" + ] + } + }, + "response": [] + }, + { + "name": "POST Course for Author (long title == long description)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Ye black spot cog Spanish Main black jack gabion smartly bilge water list. Execution dock yawl clap of thunder yo-ho-ho case shot poop deck loot hail-shot overhaul. Quarter no prey, no pay bowsprit piracy bucko driver crimp mizzenmast fire in the hole. Stern swing the lead smartly hogshead port jack bilge parley lugger. Nipper brig tackle Brethren of the Coast crimp rigging lanyard cutlass fluke. Blow the man down lass rutters Sea Legs warp gun splice the main brace ho black spot. Long clothes main sheet Davy Jones' Locker yawl ballast dead men tell no tales run a rig gally bucko. Heave down piracy topgallant snow stern chantey barkadeer weigh anchor mizzenmast.Matey mizzenmast topgallant fire ship tender black jack barque splice the main brace square-rigged. Tackle dance the hempen jig Chain Shot bilge boatswain Yellow Jack league boom aye. Grog sheet code of conduct brig boom scuppers marooned scurvy run a shot across the bow. Swing the lead no prey, no pay blow the man down reef bilged on her anchor bowsprit chase guns spirits lad. Pillage me Jack Ketch bounty schooner Pieces of Eight ballast execution dock poop deck. Scourge of the seven seas spanker run a shot across the bow boatswain Yellow Jack knave case shot measured fer yer chains six pounders. Gangway topgallant keelhaul haul wind barque cog galleon lookout Chain Shot. Execution dock Yellow Jack hang the jib lass Cat o'nine tails keelhaul list galleon long clothes. Swab provost chase guns lookout coxswain Arr spike Plate Fleet cackle fruit. Gunwalls ahoy chase tender tack bilge rat salmagundi lugger skysail. Avast ye quarter gaff lass holystone overhaul topmast skysail. Privateer gabion barque bilge rigging pillage Arr bowsprit heave down.\",\n \"description\": \"Ye black spot cog Spanish Main black jack gabion smartly bilge water list. Execution dock yawl clap of thunder yo-ho-ho case shot poop deck loot hail-shot overhaul. Quarter no prey, no pay bowsprit piracy bucko driver crimp mizzenmast fire in the hole. Stern swing the lead smartly hogshead port jack bilge parley lugger. Nipper brig tackle Brethren of the Coast crimp rigging lanyard cutlass fluke. Blow the man down lass rutters Sea Legs warp gun splice the main brace ho black spot. Long clothes main sheet Davy Jones' Locker yawl ballast dead men tell no tales run a rig gally bucko. Heave down piracy topgallant snow stern chantey barkadeer weigh anchor mizzenmast.Matey mizzenmast topgallant fire ship tender black jack barque splice the main brace square-rigged. Tackle dance the hempen jig Chain Shot bilge boatswain Yellow Jack league boom aye. Grog sheet code of conduct brig boom scuppers marooned scurvy run a shot across the bow. Swing the lead no prey, no pay blow the man down reef bilged on her anchor bowsprit chase guns spirits lad. Pillage me Jack Ketch bounty schooner Pieces of Eight ballast execution dock poop deck. Scourge of the seven seas spanker run a shot across the bow boatswain Yellow Jack knave case shot measured fer yer chains six pounders. Gangway topgallant keelhaul haul wind barque cog galleon lookout Chain Shot. Execution dock Yellow Jack hang the jib lass Cat o'nine tails keelhaul list galleon long clothes. Swab provost chase guns lookout coxswain Arr spike Plate Fleet cackle fruit. Gunwalls ahoy chase tender tack bilge rat salmagundi lugger skysail. Avast ye quarter gaff lass holystone overhaul topmast skysail. Privateer gabion barque bilge rigging pillage Arr bowsprit heave down.\"\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors/2902b665-1190-4c70-9915-b9c2d7680450/courses", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "2902b665-1190-4c70-9915-b9c2d7680450", + "courses" + ] + } + }, + "response": [] + }, + { + "name": "PUT Course for Author", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Updated title\",\n \"description\": \"Updated description\"\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "5b1c2b4d-48c7-402a-80c3-cc796ad49c6b" + ] + } + }, + "response": [] + }, + { + "name": "PUT Course for Author (with ids)", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Updated title - Test with both ids\",\n \"description\": \"Updated description - Test with both ids\",\n \"id\": \"e57b605f-8b3c-4089-b672-6ce9e6d6c23f\",\n \"authorId\": \"f74d6899-9ed2-4137-9876-66b070553f8f\"\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "5b1c2b4d-48c7-402a-80c3-cc796ad49c6b" + ] + } + }, + "response": [] + }, + { + "name": "PUT Course for Author (no description)", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Updated title\"\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "5b1c2b4d-48c7-402a-80c3-cc796ad49c6b" + ] + } + }, + "response": [] + }, + { + "name": "PUT Course for Author (unexisting author)", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Updated title\",\n \"description\": \"Updated description\"\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors/b94975d9-b640-40d4-ac11-9eb2ed1c66dc/courses/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "b94975d9-b640-40d4-ac11-9eb2ed1c66dc", + "courses", + "5b1c2b4d-48c7-402a-80c3-cc796ad49c6b" + ] + } + }, + "response": [] + }, + { + "name": "PUT Course for Author (unexisting course)", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"title\" : \"Drinking Games for Lazy Pirates\",\n\t\"description\" : \"The best drinking games for pirates that don't like to move their feet unless strictly necessary.\"\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/ea6d127c-de97-4ee5-b259-220dc314896c", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "ea6d127c-de97-4ee5-b259-220dc314896c" + ] + } + }, + "response": [] + }, + { + "name": "PUT Course for Author (invalid values)", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\": null,\n \"description\": \"Capstan gibbet poop deck smartly knave snow crow's nest tack Corsair doubloon. Dead men tell no tales Barbary Coast coxswain Arr gunwalls walk the plank tackle Gold Road yo-ho-ho lugger. Clipper lass sloop crack Jennys tea cup boatswain Pirate Round fire in the hole yard Gold Road weigh anchor. Draft measured fer yer chains boatswain fore lugsail heave down salmagundi pillage careen keel. Smartly warp run a rig grog dead men tell no tales lanyard loot bilge water coffer pink. Red ensign square-rigged ahoy gunwalls quarter knave doubloon handsomely shrouds reef sails. Belay brigantine galleon rigging nipper wherry lee parrel loaded to the gunwalls carouser. Jack Tar rope's end lee Corsair schooner barkadeer chantey rigging jack cable. No prey, no pay lee Chain Shot ye code of conduct tender Jack Tar topgallant piracy black jack. Hearties measured fer yer chains bowsprit port starboard Pieces of Eight Pirate Round pressgang black jack brig. Crimp salmagundi Brethren of the Coast poop deck coxswain quarterdeck black spot hogshead reef sails Yellow Jack. Brigantine piracy league Privateer run a shot across the bow rum lass Pirate Round Davy Jones' Locker ho. Arr yardarm walk the plank long boat hardtack gangplank bring a spring upon her cable scallywag port mizzen. Ahoy lateen sail Corsair gangplank careen warp rigging chase nipper gaff. Scallywag rutters plunder hail-shot fluke draught yo-ho-ho long clothes maroon reef sails.Capstan gibbet poop deck smartly knave snow crow's nest tack Corsair doubloon. Dead men tell no tales Barbary Coast coxswain Arr gunwalls walk the plank tackle Gold Road yo-ho-ho lugger. Clipper lass sloop crack Jennys tea cup boatswain Pirate Round fire in the hole yard Gold Road weigh anchor. Draft measured fer yer chains boatswain fore lugsail heave down salmagundi pillage careen keel. Smartly warp run a rig grog dead men tell no tales lanyard loot bilge water coffer pink. Red ensign square-rigged ahoy gunwalls quarter knave doubloon handsomely shrouds reef sails. Belay brigantine galleon rigging nipper wherry lee parrel loaded to the gunwalls carouser. Jack Tar rope's end lee Corsair schooner barkadeer chantey rigging jack cable. No prey, no pay lee Chain Shot ye code of conduct tender Jack Tar topgallant piracy black jack. Hearties measured fer yer chains bowsprit port starboard Pieces of Eight Pirate Round pressgang black jack brig. Crimp salmagundi Brethren of the Coast poop deck coxswain quarterdeck black spot hogshead reef sails Yellow Jack. Brigantine piracy league Privateer run a shot across the bow rum lass Pirate Round Davy Jones' Locker ho. Arr yardarm walk the plank long boat hardtack gangplank bring a spring upon her cable scallywag port mizzen. Ahoy lateen sail Corsair gangplank careen warp rigging chase nipper gaff. Scallywag rutters plunder hail-shot fluke draught yo-ho-ho long clothes maroon reef sails.Capstan gibbet poop deck smartly knave snow crow's nest tack Corsair doubloon. Dead men tell no tales Barbary Coast coxswain Arr gunwalls walk the plank tackle Gold Road yo-ho-ho lugger. Clipper lass sloop crack Jennys tea cup boatswain Pirate Round fire in the hole yard Gold Road weigh anchor. Draft measured fer yer chains boatswain fore lugsail heave down salmagundi pillage careen keel. Smartly warp run a rig grog dead men tell no tales lanyard loot bilge water coffer pink. Red ensign square-rigged ahoy gunwalls quarter knave doubloon handsomely shrouds reef sails. Belay brigantine galleon rigging nipper wherry lee parrel loaded to the gunwalls carouser. Jack Tar rope's end lee Corsair schooner barkadeer chantey rigging jack cable. No prey, no pay lee Chain Shot ye code of conduct tender Jack Tar topgallant piracy black jack. Hearties measured fer yer chains bowsprit port starboard Pieces of Eight Pirate Round pressgang black jack brig. Crimp salmagundi Brethren of the Coast poop deck coxswain quarterdeck black spot hogshead reef sails Yellow Jack. Brigantine piracy league Privateer run a shot across the bow rum lass Pirate Round Davy Jones' Locker ho. Arr yardarm walk the plank long boat hardtack gangplank bring a spring upon her cable scallywag port mizzen. Ahoy lateen sail Corsair gangplank careen warp rigging chase nipper gaff. Scallywag rutters plunder hail-shot fluke draught yo-ho-ho long clothes maroon reef sails.\"\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "5b1c2b4d-48c7-402a-80c3-cc796ad49c6b" + ] + } + }, + "response": [] + }, + { + "name": "PUT Course for Author (null description)", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Updated title\",\n \"description\": null\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "5b1c2b4d-48c7-402a-80c3-cc796ad49c6b" + ] + } + }, + "response": [] + }, + { + "name": "PUT Course for Author (null title == null description)", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\" : null,\n \"description\" : null\n}" + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "5b1c2b4d-48c7-402a-80c3-cc796ad49c6b" + ] + } + }, + "response": [] + }, + { + "name": "PATCH Course for Author", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json-patch+json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "[\n {\n \"op\": \"replace\",\n \"path\": \"/title\",\n \"value\": \"Updated title\"\n }\n]" + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "5b1c2b4d-48c7-402a-80c3-cc796ad49c6b" + ] + } + }, + "response": [] + }, + { + "name": "PATCH Course for Author (multiple)", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json-patch+json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "[\n {\n \"op\": \"replace\",\n \"path\": \"/title\",\n \"value\": \"Another updated title\"\n },\n {\n \"op\": \"replace\",\n \"path\": \"/description\",\n \"value\": \"Updated description\"\n }\n]" + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "5b1c2b4d-48c7-402a-80c3-cc796ad49c6b" + ] + } + }, + "response": [] + }, + { + "name": "PATCH Course for Author (remove)", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json-patch+json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "[\n {\n \"op\": \"remove\",\n \"path\": \"/description\"\n }\n]" + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "5b1c2b4d-48c7-402a-80c3-cc796ad49c6b" + ] + } + }, + "response": [] + }, + { + "name": "PATCH Course for Author (copy and add)", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json-patch+json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "[\n {\n \"op\": \"add\",\n \"path\": \"/description\",\n \"value\": \"New description\"\n },\n {\n \"op\": \"copy\",\n \"from\": \"/description\",\n \"path\": \"/title\"\n }\n]" + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "5b1c2b4d-48c7-402a-80c3-cc796ad49c6b" + ] + } + }, + "response": [] + }, + { + "name": "PATCH Course for Author (unexisting author)", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json-patch+json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "[\n {\n \"op\": \"replace\",\n \"path\": \"/title\",\n \"value\": \"Updated title\"\n }\n]" + }, + "url": { + "raw": "http://localhost:51044/api/authors/b94975d9-b640-40d4-ac11-9eb2ed1c66dc/courses/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "b94975d9-b640-40d4-ac11-9eb2ed1c66dc", + "courses", + "5b1c2b4d-48c7-402a-80c3-cc796ad49c6b" + ] + } + }, + "response": [] + }, + { + "name": "PATCH Course for Author (unexisting course)", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json-patch+json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "[\n {\n \"op\": \"replace\",\n \"path\": \"/title\",\n \"value\": \"Updated title\"\n },\n {\n \"op\": \"replace\",\n \"path\": \"/description\",\n \"value\": \"Updated description\"\n }\n]" + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/ea6d127c-de97-4ee5-b259-220dc314896c", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "ea6d127c-de97-4ee5-b259-220dc314896c" + ] + } + }, + "response": [] + }, + { + "name": "PATCH Course for Author (remove description)", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json-patch+json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "[\n {\n \"op\": \"remove\",\n \"path\": \"/description\"\n }\n]" + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "5b1c2b4d-48c7-402a-80c3-cc796ad49c6b" + ] + } + }, + "response": [] + }, + { + "name": "PATCH Course for Author (remove unexisting property)", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json-patch+json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "[\n {\n \"op\": \"remove\",\n \"path\": \"/thisdoesnotexist\"\n }\n]" + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "5b1c2b4d-48c7-402a-80c3-cc796ad49c6b" + ] + } + }, + "response": [] + }, + { + "name": "PATCH Course for Author (UPSERT - no title)", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json-patch+json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "[\n {\n \"op\": \"replace\",\n \"path\": \"/description\",\n \"value\": \"Updated description\"\n }\n]" + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/75bf303e-6dc4-4b00-81fd-f896a6379f5f", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "75bf303e-6dc4-4b00-81fd-f896a6379f5f" + ] + } + }, + "response": [] + }, + { + "name": "PATCH Course for Author (UPSERT - remove unexisting property)", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json-patch+json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "[\n {\n \"op\": \"add\",\n \"path\": \"/thisdoesnotexist\",\n \"value\": \"new value\"\n }\n]" + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/75bf303e-6dc4-4b00-81fd-f896a6379f5f", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "75bf303e-6dc4-4b00-81fd-f896a6379f5f" + ] + } + }, + "response": [] + }, + { + "name": "DELETE Course for Author", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/d8663e5e-7494-4f81-8739-6e0de1bea7ee", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "d8663e5e-7494-4f81-8739-6e0de1bea7ee" + ] + } + }, + "response": [] + }, + { + "name": "DELETE Course for Author (unexisting Author)", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "http://localhost:51044/api/authors/787f6625-6048-43d7-b64e-bf3d02f0132d/courses/70a1f9b9-0a37-4c1a-99b1-c7709fc64167", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "787f6625-6048-43d7-b64e-bf3d02f0132d", + "courses", + "70a1f9b9-0a37-4c1a-99b1-c7709fc64167" + ] + } + }, + "response": [] + }, + { + "name": "DELETE Course for Author (unexisting Course)", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35/courses/3f946dbe-edf3-4c44-baef-b683bc355a0f", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "courses", + "3f946dbe-edf3-4c44-baef-b683bc355a0f" + ] + } + }, + "response": [] + }, + { + "name": "DELETE Author", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "http://localhost:51044/api/authors/d28888e9-2ba9-473a-a40f-e38cb54f9b35", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "51044", + "path": [ + "api", + "authors", + "d28888e9-2ba9-473a-a40f-e38cb54f9b35" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Controllers/AuthorCollectionsController.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Controllers/AuthorCollectionsController.cs new file mode 100644 index 0000000..09c86be --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Controllers/AuthorCollectionsController.cs @@ -0,0 +1,72 @@ +using AutoMapper; +using CourseLibrary.API.Helpers; +using CourseLibrary.API.Models; +using CourseLibrary.API.Services; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace CourseLibrary.API.Controllers +{ + [ApiController] + [Route("api/authorcollections")] + public class AuthorCollectionsController : ControllerBase + { + private readonly ICourseLibraryRepository _courseLibraryRepository; + private readonly IMapper _mapper; + + public AuthorCollectionsController(ICourseLibraryRepository courseLibraryRepository, + IMapper mapper) + { + _courseLibraryRepository = courseLibraryRepository ?? + throw new ArgumentNullException(nameof(courseLibraryRepository)); + _mapper = mapper ?? + throw new ArgumentNullException(nameof(mapper)); + } + + [HttpGet("({ids})", Name ="GetAuthorCollection")] + public IActionResult GetAuthorCollection( + [FromRoute] + [ModelBinder(BinderType = typeof(ArrayModelBinder))] IEnumerable ids) + { + if (ids == null) + { + return BadRequest(); + } + + var authorEntities = _courseLibraryRepository.GetAuthors(ids); + + if (ids.Count() != authorEntities.Count()) + { + return NotFound(); + } + + var authorsToReturn = _mapper.Map>(authorEntities); + + return Ok(authorsToReturn); + } + + + [HttpPost] + public ActionResult> CreateAuthorCollection( + IEnumerable authorCollection) + { + var authorEntities = _mapper.Map>(authorCollection); + foreach (var author in authorEntities) + { + _courseLibraryRepository.AddAuthor(author); + } + + _courseLibraryRepository.Save(); + + var authorCollectionToReturn = _mapper.Map>(authorEntities); + var idsAsString = string.Join(",", authorCollectionToReturn.Select(a => a.Id)); + return CreatedAtRoute("GetAuthorCollection", + new { ids = idsAsString }, + authorCollectionToReturn); + } + } +} + diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Controllers/AuthorsController.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Controllers/AuthorsController.cs new file mode 100644 index 0000000..0ff742d --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Controllers/AuthorsController.cs @@ -0,0 +1,89 @@ +using AutoMapper; +using CourseLibrary.API.Helpers; +using CourseLibrary.API.Models; +using CourseLibrary.API.ResourceParameters; +using CourseLibrary.API.Services; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace CourseLibrary.API.Controllers +{ + [ApiController] + [Route("api/authors")] + public class AuthorsController : ControllerBase + { + private readonly ICourseLibraryRepository _courseLibraryRepository; + private readonly IMapper _mapper; + + public AuthorsController(ICourseLibraryRepository courseLibraryRepository, + IMapper mapper) + { + _courseLibraryRepository = courseLibraryRepository ?? + throw new ArgumentNullException(nameof(courseLibraryRepository)); + _mapper = mapper ?? + throw new ArgumentNullException(nameof(mapper)); + } + + [HttpGet()] + [HttpHead] + public ActionResult> GetAuthors( + [FromQuery] AuthorsResourceParameters authorsResourceParameters) + { + var authorsFromRepo = _courseLibraryRepository.GetAuthors(authorsResourceParameters); + return Ok(_mapper.Map>(authorsFromRepo)); + } + + [HttpGet("{authorId}", Name ="GetAuthor")] + public IActionResult GetAuthor(Guid authorId) + { + var authorFromRepo = _courseLibraryRepository.GetAuthor(authorId); + + if (authorFromRepo == null) + { + return NotFound(); + } + + return Ok(_mapper.Map(authorFromRepo)); + } + + [HttpPost] + public ActionResult CreateAuthor(AuthorForCreationDto author) + { + var authorEntity = _mapper.Map(author); + _courseLibraryRepository.AddAuthor(authorEntity); + _courseLibraryRepository.Save(); + + var authorToReturn = _mapper.Map(authorEntity); + return CreatedAtRoute("GetAuthor", + new { authorId = authorToReturn.Id }, + authorToReturn); + } + + [HttpOptions] + public IActionResult GetAuthorsOptions() + { + Response.Headers.Add("Allow", "GET,OPTIONS,POST"); + return Ok(); + } + + [HttpDelete("{authorId}")] + public ActionResult DeleteAuthor(Guid authorId) + { + var authorFromRepo = _courseLibraryRepository.GetAuthor(authorId); + + if (authorFromRepo == null) + { + return NotFound(); + } + + _courseLibraryRepository.DeleteAuthor(authorFromRepo); + + _courseLibraryRepository.Save(); + + return NoContent(); + } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Controllers/CoursesController.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Controllers/CoursesController.cs new file mode 100644 index 0000000..e322def --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Controllers/CoursesController.cs @@ -0,0 +1,203 @@ +using AutoMapper; +using CourseLibrary.API.Models; +using CourseLibrary.API.Services; +using Microsoft.AspNetCore.JsonPatch; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace CourseLibrary.API.Controllers +{ + [ApiController] + [Route("api/authors/{authorId}/courses")] + public class CoursesController : ControllerBase + { + private readonly ICourseLibraryRepository _courseLibraryRepository; + private readonly IMapper _mapper; + + public CoursesController(ICourseLibraryRepository courseLibraryRepository, + IMapper mapper) + { + _courseLibraryRepository = courseLibraryRepository ?? + throw new ArgumentNullException(nameof(courseLibraryRepository)); + _mapper = mapper ?? + throw new ArgumentNullException(nameof(mapper)); + } + + [HttpGet] + public ActionResult> GetCoursesForAuthor(Guid authorId) + { + if (!_courseLibraryRepository.AuthorExists(authorId)) + { + return NotFound(); + } + + var coursesForAuthorFromRepo = _courseLibraryRepository.GetCourses(authorId); + return Ok(_mapper.Map>(coursesForAuthorFromRepo)); + } + + [HttpGet("{courseId}", Name = "GetCourseForAuthor")] + public ActionResult GetCourseForAuthor(Guid authorId, Guid courseId) + { + if (!_courseLibraryRepository.AuthorExists(authorId)) + { + return NotFound(); + } + + var courseForAuthorFromRepo = _courseLibraryRepository.GetCourse(authorId, courseId); + + if (courseForAuthorFromRepo == null) + { + return NotFound(); + } + + return Ok(_mapper.Map(courseForAuthorFromRepo)); + } + + [HttpPost] + public ActionResult CreateCourseForAuthor( + Guid authorId, CourseForCreationDto course) + { + if (!_courseLibraryRepository.AuthorExists(authorId)) + { + return NotFound(); + } + + var courseEntity = _mapper.Map(course); + _courseLibraryRepository.AddCourse(authorId, courseEntity); + _courseLibraryRepository.Save(); + + var courseToReturn = _mapper.Map(courseEntity); + return CreatedAtRoute("GetCourseForAuthor", + new { authorId = authorId, courseId = courseToReturn.Id }, + courseToReturn); + } + + [HttpPut("{courseId}")] + public IActionResult UpdateCourseForAuthor(Guid authorId, + Guid courseId, + CourseForUpdateDto course) + { + if (!_courseLibraryRepository.AuthorExists(authorId)) + { + return NotFound(); + } + + var courseForAuthorFromRepo = _courseLibraryRepository.GetCourse(authorId, courseId); + + if (courseForAuthorFromRepo == null) + { + var courseToAdd = _mapper.Map(course); + courseToAdd.Id = courseId; + + _courseLibraryRepository.AddCourse(authorId, courseToAdd); + + _courseLibraryRepository.Save(); + + var courseToReturn = _mapper.Map(courseToAdd); + + return CreatedAtRoute("GetCourseForAuthor", + new { authorId, courseId = courseToReturn.Id }, + courseToReturn); + } + + // map the entity to a CourseForUpdateDto + // apply the updated field values to that dto + // map the CourseForUpdateDto back to an entity + _mapper.Map(course, courseForAuthorFromRepo); + + _courseLibraryRepository.UpdateCourse(courseForAuthorFromRepo); + + _courseLibraryRepository.Save(); + return NoContent(); + } + + [HttpPatch("{courseId}")] + public ActionResult PartiallyUpdateCourseForAuthor(Guid authorId, + Guid courseId, + JsonPatchDocument patchDocument) + { + if (!_courseLibraryRepository.AuthorExists(authorId)) + { + return NotFound(); + } + + var courseForAuthorFromRepo = _courseLibraryRepository.GetCourse(authorId, courseId); + + if (courseForAuthorFromRepo == null) + { + var courseDto = new CourseForUpdateDto(); + patchDocument.ApplyTo(courseDto, ModelState); + + if (!TryValidateModel(courseDto)) + { + return ValidationProblem(ModelState); + } + + var courseToAdd = _mapper.Map(courseDto); + courseToAdd.Id = courseId; + + _courseLibraryRepository.AddCourse(authorId, courseToAdd); + _courseLibraryRepository.Save(); + + var courseToReturn = _mapper.Map(courseToAdd); + + return CreatedAtRoute("GetCourseForAuthor", + new { authorId, courseId = courseToReturn.Id }, + courseToReturn); + } + + var courseToPatch = _mapper.Map(courseForAuthorFromRepo); + // add validation + patchDocument.ApplyTo(courseToPatch, ModelState); + + if (!TryValidateModel(courseToPatch)) + { + return ValidationProblem(ModelState); + } + + _mapper.Map(courseToPatch, courseForAuthorFromRepo); + + _courseLibraryRepository.UpdateCourse(courseForAuthorFromRepo); + + _courseLibraryRepository.Save(); + + return NoContent(); + } + + [HttpDelete("{courseId}")] + public ActionResult DeleteCourseForAuthor(Guid authorId, Guid courseId) + { + if (!_courseLibraryRepository.AuthorExists(authorId)) + { + return NotFound(); + } + + var courseForAuthorFromRepo = _courseLibraryRepository.GetCourse(authorId, courseId); + + if (courseForAuthorFromRepo == null) + { + return NotFound(); + } + + _courseLibraryRepository.DeleteCourse(courseForAuthorFromRepo); + _courseLibraryRepository.Save(); + + return NoContent(); + } + + public override ActionResult ValidationProblem( + [ActionResultObjectValue] ModelStateDictionary modelStateDictionary) + { + var options = HttpContext.RequestServices + .GetRequiredService>(); + return (ActionResult)options.Value.InvalidModelStateResponseFactory(ControllerContext); + } + } +} \ No newline at end of file diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/CourseLibrary.API.csproj b/Finished sample/CourseLibrary/CourseLibrary.API/CourseLibrary.API.csproj new file mode 100644 index 0000000..fa6b875 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/CourseLibrary.API.csproj @@ -0,0 +1,23 @@ + + + + netcoreapp3.0 + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/DbContexts/CourseLibraryContext.cs b/Finished sample/CourseLibrary/CourseLibrary.API/DbContexts/CourseLibraryContext.cs new file mode 100644 index 0000000..9adc91c --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/DbContexts/CourseLibraryContext.cs @@ -0,0 +1,113 @@ +using CourseLibrary.API.Entities; +using Microsoft.EntityFrameworkCore; +using System; + +namespace CourseLibrary.API.DbContexts +{ + public class CourseLibraryContext : DbContext + { + public CourseLibraryContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Authors { get; set; } + public DbSet Courses { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // seed the database with dummy data + modelBuilder.Entity().HasData( + new Author() + { + Id = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), + FirstName = "Berry", + LastName = "Griffin Beak Eldritch", + DateOfBirth = new DateTime(1650, 7, 23), + MainCategory = "Ships" + }, + new Author() + { + Id = Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), + FirstName = "Nancy", + LastName = "Swashbuckler Rye", + DateOfBirth = new DateTime(1668, 5, 21), + MainCategory = "Rum" + }, + new Author() + { + Id = Guid.Parse("2902b665-1190-4c70-9915-b9c2d7680450"), + FirstName = "Eli", + LastName = "Ivory Bones Sweet", + DateOfBirth = new DateTime(1701, 12, 16), + MainCategory = "Singing" + }, + new Author() + { + Id = Guid.Parse("102b566b-ba1f-404c-b2df-e2cde39ade09"), + FirstName = "Arnold", + LastName = "The Unseen Stafford", + DateOfBirth = new DateTime(1702, 3, 6), + MainCategory = "Singing" + }, + new Author() + { + Id = Guid.Parse("5b3621c0-7b12-4e80-9c8b-3398cba7ee05"), + FirstName = "Seabury", + LastName = "Toxic Reyson", + DateOfBirth = new DateTime(1690, 11, 23), + MainCategory = "Maps" + }, + new Author() + { + Id = Guid.Parse("2aadd2df-7caf-45ab-9355-7f6332985a87"), + FirstName = "Rutherford", + LastName = "Fearless Cloven", + DateOfBirth = new DateTime(1723, 4, 5), + MainCategory = "General debauchery" + }, + new Author() + { + Id = Guid.Parse("2ee49fe3-edf2-4f91-8409-3eb25ce6ca51"), + FirstName = "Atherton", + LastName = "Crow Ridley", + DateOfBirth = new DateTime(1721, 10, 11), + MainCategory = "Rum" + } + ); + + modelBuilder.Entity().HasData( + new Course + { + Id = Guid.Parse("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), + AuthorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), + Title = "Commandeering a Ship Without Getting Caught", + Description = "Commandeering a ship in rough waters isn't easy. Commandeering it without getting caught is even harder. In this course you'll learn how to sail away and avoid those pesky musketeers." + }, + new Course + { + Id = Guid.Parse("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), + AuthorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), + Title = "Overthrowing Mutiny", + Description = "In this course, the author provides tips to avoid, or, if needed, overthrow pirate mutiny." + }, + new Course + { + Id = Guid.Parse("d173e20d-159e-4127-9ce9-b0ac2564ad97"), + AuthorId = Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), + Title = "Avoiding Brawls While Drinking as Much Rum as You Desire", + Description = "Every good pirate loves rum, but it also has a tendency to get you into trouble. In this course you'll learn how to avoid that. This new exclusive edition includes an additional chapter on how to run fast without falling while drunk." + }, + new Course + { + Id = Guid.Parse("40ff5488-fdab-45b5-bc3a-14302d59869a"), + AuthorId = Guid.Parse("2902b665-1190-4c70-9915-b9c2d7680450"), + Title = "Singalong Pirate Hits", + Description = "In this course you'll learn how to sing all-time favourite pirate songs without sounding like you actually know the words or how to hold a note." + } + ); + + base.OnModelCreating(modelBuilder); + } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Entities/Author.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Entities/Author.cs new file mode 100644 index 0000000..5f1d9ef --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Entities/Author.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace CourseLibrary.API.Entities +{ + public class Author + { + [Key] + public Guid Id { get; set; } + + [Required] + [MaxLength(50)] + public string FirstName { get; set; } + + [Required] + [MaxLength(50)] + public string LastName { get; set; } + + [Required] + public DateTimeOffset DateOfBirth { get; set; } + + [Required] + [MaxLength(50)] + public string MainCategory { get; set; } + + public ICollection Courses { get; set; } + = new List(); + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Entities/Course.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Entities/Course.cs new file mode 100644 index 0000000..7629a75 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Entities/Course.cs @@ -0,0 +1,24 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace CourseLibrary.API.Entities +{ + public class Course + { + [Key] + public Guid Id { get; set; } + + [Required] + [MaxLength(100)] + public string Title { get; set; } + + [MaxLength(1500)] + public string Description { get; set; } + + [ForeignKey("AuthorId")] + public Author Author { get; set; } + + public Guid AuthorId { get; set; } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Helpers/ArrayModelBinder.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Helpers/ArrayModelBinder.cs new file mode 100644 index 0000000..4e121c3 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Helpers/ArrayModelBinder.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace CourseLibrary.API.Helpers +{ + public class ArrayModelBinder : IModelBinder + { + public Task BindModelAsync(ModelBindingContext bindingContext) + { + // Our binder works only on enumerable types + if (!bindingContext.ModelMetadata.IsEnumerableType) + { + bindingContext.Result = ModelBindingResult.Failed(); + return Task.CompletedTask; + } + + // Get the inputted value through the value provider + var value = bindingContext.ValueProvider + .GetValue(bindingContext.ModelName).ToString(); + + // If that value is null or whitespace, we return null + if (string.IsNullOrWhiteSpace(value)) + { + bindingContext.Result = ModelBindingResult.Success(null); + return Task.CompletedTask; + } + + // The value isn't null or whitespace, + // and the type of the model is enumerable. + // Get the enumerable's type, and a converter + var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0]; + var converter = TypeDescriptor.GetConverter(elementType); + + // Convert each item in the value list to the enumerable type + var values = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => converter.ConvertFromString(x.Trim())) + .ToArray(); + + // Create an array of that type, and set it as the Model value + var typedValues = Array.CreateInstance(elementType, values.Length); + values.CopyTo(typedValues, 0); + bindingContext.Model = typedValues; + + // return a successful result, passing in the Model + bindingContext.Result = ModelBindingResult.Success(bindingContext.Model); + return Task.CompletedTask; + } + } + +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Helpers/DateTimeOffsetExtensions.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Helpers/DateTimeOffsetExtensions.cs new file mode 100644 index 0000000..8080581 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Helpers/DateTimeOffsetExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace CourseLibrary.API.Helpers +{ + public static class DateTimeOffsetExtensions + { + public static int GetCurrentAge(this DateTimeOffset dateTimeOffset) + { + var currentDate = DateTime.UtcNow; + int age = currentDate.Year - dateTimeOffset.Year; + + if (currentDate < dateTimeOffset.AddYears(age)) + { + age--; + } + + return age; + } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Migrations/CourseLibraryContextModelSnapshot.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Migrations/CourseLibraryContextModelSnapshot.cs new file mode 100644 index 0000000..23e69bc --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Migrations/CourseLibraryContextModelSnapshot.cs @@ -0,0 +1,168 @@ +// +using System; +using CourseLibrary.API.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace CourseLibrary.API.Migrations +{ + [DbContext(typeof(CourseLibraryContext))] + partial class CourseLibraryContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.0.0-preview6.19304.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("CourseLibrary.API.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateOfBirth"); + + b.Property("DateOfDeath"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50); + + b.Property("MainCategory") + .IsRequired() + .HasMaxLength(50); + + b.HasKey("Id"); + + b.ToTable("Authors"); + + b.HasData( + new + { + Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), + DateOfBirth = new DateTimeOffset(new DateTime(1650, 7, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), + FirstName = "Berry", + LastName = "Griffin Beak Eldritch", + MainCategory = "Ships" + }, + new + { + Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), + DateOfBirth = new DateTimeOffset(new DateTime(1668, 5, 21, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), + FirstName = "Nancy", + LastName = "Swashbuckler Rye", + MainCategory = "Rum" + }, + new + { + Id = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), + DateOfBirth = new DateTimeOffset(new DateTime(1701, 12, 16, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), + FirstName = "Eli", + LastName = "Ivory Bones Sweet", + MainCategory = "Singing" + }, + new + { + Id = new Guid("102b566b-ba1f-404c-b2df-e2cde39ade09"), + DateOfBirth = new DateTimeOffset(new DateTime(1702, 3, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), + FirstName = "Arnold", + LastName = "The Unseen Stafford", + MainCategory = "Singing" + }, + new + { + Id = new Guid("5b3621c0-7b12-4e80-9c8b-3398cba7ee05"), + DateOfBirth = new DateTimeOffset(new DateTime(1690, 11, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), + FirstName = "Seabury", + LastName = "Toxic Reyson", + MainCategory = "Maps" + }, + new + { + Id = new Guid("2aadd2df-7caf-45ab-9355-7f6332985a87"), + DateOfBirth = new DateTimeOffset(new DateTime(1723, 4, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), + FirstName = "Rutherford", + LastName = "Fearless Cloven", + MainCategory = "General debauchery" + }, + new + { + Id = new Guid("2ee49fe3-edf2-4f91-8409-3eb25ce6ca51"), + DateOfBirth = new DateTimeOffset(new DateTime(1721, 10, 11, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), + FirstName = "Atherton", + LastName = "Crow Ridley", + MainCategory = "Rum" + }); + }); + + modelBuilder.Entity("CourseLibrary.API.Entities.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("Description") + .HasMaxLength(1500); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("Courses"); + + b.HasData( + new + { + Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), + AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), + Description = "Commandeering a ship in rough waters isn't easy. Commandeering it without getting caught is even harder. In this course you'll learn how to sail away and avoid those pesky musketeers.", + Title = "Commandeering a Ship Without Getting Caught" + }, + new + { + Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), + AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), + Description = "In this course, the author provides tips to avoid, or, if needed, overthrow pirate mutiny.", + Title = "Overthrowing Mutiny" + }, + new + { + Id = new Guid("d173e20d-159e-4127-9ce9-b0ac2564ad97"), + AuthorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), + Description = "Every good pirate loves rum, but it also has a tendency to get you into trouble. In this course you'll learn how to avoid that. This new exclusive edition includes an additional chapter on how to run fast without falling while drunk.", + Title = "Avoiding Brawls While Drinking as Much Rum as You Desire" + }, + new + { + Id = new Guid("40ff5488-fdab-45b5-bc3a-14302d59869a"), + AuthorId = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), + Description = "In this course you'll learn how to sing all-time favourite pirate songs without sounding like you actually know the words or how to hold a note.", + Title = "Singalong Pirate Hits" + }); + }); + + modelBuilder.Entity("CourseLibrary.API.Entities.Course", b => + { + b.HasOne("CourseLibrary.API.Entities.Author", "Author") + .WithMany("Courses") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Migrations/InitialMigration.Designer.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Migrations/InitialMigration.Designer.cs new file mode 100644 index 0000000..af2724e --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Migrations/InitialMigration.Designer.cs @@ -0,0 +1,208 @@ +// +using System; +using CourseLibrary.API.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace CourseLibrary.API.Migrations +{ + [DbContext(typeof(CourseLibraryContext))] + [Migration("20190731122611_InitialMigration")] + partial class InitialMigration + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.0.0-preview7.19362.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("CourseLibrary.API.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DateOfBirth"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50); + + b.Property("MainCategory") + .IsRequired() + .HasMaxLength(50); + + b.HasKey("Id"); + + b.ToTable("Authors"); + + b.HasData( + new + { + Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), + DateOfBirth = new DateTimeOffset(new DateTime(1970, 7, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), + FirstName = "Berry", + LastName = "Griffin Beak Eldritch", + MainCategory = "Ships" + }, + new + { + Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), + DateOfBirth = new DateTimeOffset(new DateTime(1968, 5, 21, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), + FirstName = "Nancy", + LastName = "Swashbuckler Rye", + MainCategory = "Rum" + }, + new + { + Id = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), + DateOfBirth = new DateTimeOffset(new DateTime(1991, 12, 16, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), + FirstName = "Eli", + LastName = "Ivory Bones Sweet", + MainCategory = "Singing" + }, + new + { + Id = new Guid("102b566b-ba1f-404c-b2df-e2cde39ade09"), + DateOfBirth = new DateTimeOffset(new DateTime(1984, 3, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), + FirstName = "Arnold", + LastName = "The Unseen Stafford", + MainCategory = "Singing" + }, + new + { + Id = new Guid("5b3621c0-7b12-4e80-9c8b-3398cba7ee05"), + DateOfBirth = new DateTimeOffset(new DateTime(1990, 11, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), + FirstName = "Seabury", + LastName = "Toxic Reyson", + MainCategory = "Maps" + }, + new + { + Id = new Guid("2aadd2df-7caf-45ab-9355-7f6332985a87"), + DateOfBirth = new DateTimeOffset(new DateTime(1978, 4, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), + FirstName = "Rutherford", + LastName = "Fearless Cloven", + MainCategory = "General debauchery" + }, + new + { + Id = new Guid("2ee49fe3-edf2-4f91-8409-3eb25ce6ca51"), + DateOfBirth = new DateTimeOffset(new DateTime(1959, 10, 11, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), + FirstName = "Atherton", + LastName = "Crow Ridley", + MainCategory = "Rum" + }, + new + { + Id = new Guid("71838f8b-6ab3-4539-9e67-4e77b8ede1c0"), + DateOfBirth = new DateTimeOffset(new DateTime(1969, 8, 11, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), + FirstName = "Huxford", + LastName = "The Hawk Morris", + MainCategory = "Maps" + }, + new + { + Id = new Guid("119f9ccb-149d-4d3c-ad4f-40100f38e918"), + DateOfBirth = new DateTimeOffset(new DateTime(1972, 1, 8, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), + FirstName = "Dwennon", + LastName = "Rigger Quye", + MainCategory = "Maps" + }, + new + { + Id = new Guid("28c1db41-f104-46e6-8943-d31c0291e0e3"), + DateOfBirth = new DateTimeOffset(new DateTime(1982, 5, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), + FirstName = "Rushford", + LastName = "Subtle Asema", + MainCategory = "Rum" + }, + new + { + Id = new Guid("d94a64c2-2e8f-4162-9976-0ffe03d30767"), + DateOfBirth = new DateTimeOffset(new DateTime(1976, 7, 12, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), + FirstName = "Hagley", + LastName = "Imposter Grendel", + MainCategory = "Singing" + }, + new + { + Id = new Guid("380c2c6b-0d1c-4b82-9d83-3cf635a3e62b"), + DateOfBirth = new DateTimeOffset(new DateTime(1977, 2, 8, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), + FirstName = "Mabel", + LastName = "Barnacle Grendel", + MainCategory = "Maps" + }); + }); + + modelBuilder.Entity("CourseLibrary.API.Entities.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("Description") + .HasMaxLength(1500); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("Courses"); + + b.HasData( + new + { + Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), + AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), + Description = "Commandeering a ship in rough waters isn't easy. Commandeering it without getting caught is even harder. In this course you'll learn how to sail away and avoid those pesky musketeers.", + Title = "Commandeering a Ship Without Getting Caught" + }, + new + { + Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), + AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), + Description = "In this course, the author provides tips to avoid, or, if needed, overthrow pirate mutiny.", + Title = "Overthrowing Mutiny" + }, + new + { + Id = new Guid("d173e20d-159e-4127-9ce9-b0ac2564ad97"), + AuthorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), + Description = "Every good pirate loves rum, but it also has a tendency to get you into trouble. In this course you'll learn how to avoid that. This new exclusive edition includes an additional chapter on how to run fast without falling while drunk.", + Title = "Avoiding Brawls While Drinking as Much Rum as You Desire" + }, + new + { + Id = new Guid("40ff5488-fdab-45b5-bc3a-14302d59869a"), + AuthorId = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), + Description = "In this course you'll learn how to sing all-time favourite pirate songs without sounding like you actually know the words or how to hold a note.", + Title = "Singalong Pirate Hits" + }); + }); + + modelBuilder.Entity("CourseLibrary.API.Entities.Course", b => + { + b.HasOne("CourseLibrary.API.Entities.Author", "Author") + .WithMany("Courses") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Migrations/InitialMigration.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Migrations/InitialMigration.cs new file mode 100644 index 0000000..e9098a0 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Migrations/InitialMigration.cs @@ -0,0 +1,90 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace CourseLibrary.API.Migrations +{ + public partial class InitialMigration : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Authors", + columns: table => new + { + Id = table.Column(nullable: false), + FirstName = table.Column(maxLength: 50, nullable: false), + LastName = table.Column(maxLength: 50, nullable: false), + DateOfBirth = table.Column(nullable: false), + MainCategory = table.Column(maxLength: 50, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Authors", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Courses", + columns: table => new + { + Id = table.Column(nullable: false), + Title = table.Column(maxLength: 100, nullable: false), + Description = table.Column(maxLength: 1500, nullable: true), + AuthorId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Courses", x => x.Id); + table.ForeignKey( + name: "FK_Courses_Authors_AuthorId", + column: x => x.AuthorId, + principalTable: "Authors", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "Authors", + columns: new[] { "Id", "DateOfBirth", "FirstName", "LastName", "MainCategory" }, + values: new object[,] + { + { new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), new DateTimeOffset(new DateTime(1970, 7, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), "Berry", "Griffin Beak Eldritch", "Ships" }, + { new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), new DateTimeOffset(new DateTime(1968, 5, 21, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), "Nancy", "Swashbuckler Rye", "Rum" }, + { new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), new DateTimeOffset(new DateTime(1991, 12, 16, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), "Eli", "Ivory Bones Sweet", "Singing" }, + { new Guid("102b566b-ba1f-404c-b2df-e2cde39ade09"), new DateTimeOffset(new DateTime(1984, 3, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), "Arnold", "The Unseen Stafford", "Singing" }, + { new Guid("5b3621c0-7b12-4e80-9c8b-3398cba7ee05"), new DateTimeOffset(new DateTime(1990, 11, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), "Seabury", "Toxic Reyson", "Maps" }, + { new Guid("2aadd2df-7caf-45ab-9355-7f6332985a87"), new DateTimeOffset(new DateTime(1978, 4, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), "Rutherford", "Fearless Cloven", "General debauchery" }, + { new Guid("2ee49fe3-edf2-4f91-8409-3eb25ce6ca51"), new DateTimeOffset(new DateTime(1959, 10, 11, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), "Atherton", "Crow Ridley", "Rum" }, + { new Guid("71838f8b-6ab3-4539-9e67-4e77b8ede1c0"), new DateTimeOffset(new DateTime(1969, 8, 11, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), "Huxford", "The Hawk Morris", "Maps" }, + { new Guid("119f9ccb-149d-4d3c-ad4f-40100f38e918"), new DateTimeOffset(new DateTime(1972, 1, 8, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), "Dwennon", "Rigger Quye", "Maps" }, + { new Guid("28c1db41-f104-46e6-8943-d31c0291e0e3"), new DateTimeOffset(new DateTime(1982, 5, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), "Rushford", "Subtle Asema", "Rum" }, + { new Guid("d94a64c2-2e8f-4162-9976-0ffe03d30767"), new DateTimeOffset(new DateTime(1976, 7, 12, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), "Hagley", "Imposter Grendel", "Singing" }, + { new Guid("380c2c6b-0d1c-4b82-9d83-3cf635a3e62b"), new DateTimeOffset(new DateTime(1977, 2, 8, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), "Mabel", "Barnacle Grendel", "Maps" } + }); + + migrationBuilder.InsertData( + table: "Courses", + columns: new[] { "Id", "AuthorId", "Description", "Title" }, + values: new object[,] + { + { new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), "Commandeering a ship in rough waters isn't easy. Commandeering it without getting caught is even harder. In this course you'll learn how to sail away and avoid those pesky musketeers.", "Commandeering a Ship Without Getting Caught" }, + { new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), "In this course, the author provides tips to avoid, or, if needed, overthrow pirate mutiny.", "Overthrowing Mutiny" }, + { new Guid("d173e20d-159e-4127-9ce9-b0ac2564ad97"), new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), "Every good pirate loves rum, but it also has a tendency to get you into trouble. In this course you'll learn how to avoid that. This new exclusive edition includes an additional chapter on how to run fast without falling while drunk.", "Avoiding Brawls While Drinking as Much Rum as You Desire" }, + { new Guid("40ff5488-fdab-45b5-bc3a-14302d59869a"), new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), "In this course you'll learn how to sing all-time favourite pirate songs without sounding like you actually know the words or how to hold a note.", "Singalong Pirate Hits" } + }); + + migrationBuilder.CreateIndex( + name: "IX_Courses_AuthorId", + table: "Courses", + column: "AuthorId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Courses"); + + migrationBuilder.DropTable( + name: "Authors"); + } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Models/AuthorDto.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Models/AuthorDto.cs new file mode 100644 index 0000000..50db640 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Models/AuthorDto.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace CourseLibrary.API.Models +{ + public class AuthorDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public int Age { get; set; } + public string MainCategory { get; set; } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Models/AuthorForCreationDto.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Models/AuthorForCreationDto.cs new file mode 100644 index 0000000..96dd6a1 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Models/AuthorForCreationDto.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace CourseLibrary.API.Models +{ + public class AuthorForCreationDto + { + public string FirstName { get; set; } + public string LastName { get; set; } + public DateTimeOffset DateOfBirth { get; set; } + public string MainCategory { get; set; } + public ICollection Courses { get; set; } + = new List(); + + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Models/CourseDto.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Models/CourseDto.cs new file mode 100644 index 0000000..e96d5ab --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Models/CourseDto.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace CourseLibrary.API.Models +{ + public class CourseDto + { + public Guid Id { get; set; } + + public string Title { get; set; } + + public string Description { get; set; } + + public Guid AuthorId { get; set; } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Models/CourseForCreationDto.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Models/CourseForCreationDto.cs new file mode 100644 index 0000000..9218c6a --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Models/CourseForCreationDto.cs @@ -0,0 +1,12 @@ +using CourseLibrary.API.ValidationAttributes; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace CourseLibrary.API.Models +{ + public class CourseForCreationDto : CourseForManipulationDto + { } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Models/CourseForManipulationDto.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Models/CourseForManipulationDto.cs new file mode 100644 index 0000000..b441104 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Models/CourseForManipulationDto.cs @@ -0,0 +1,21 @@ +using CourseLibrary.API.ValidationAttributes; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace CourseLibrary.API.Models +{ + [CourseTitleMustBeDifferentFromDescription( + ErrorMessage = "Title must be different from description.")] + public abstract class CourseForManipulationDto + { + [Required(ErrorMessage = "You should fill out a title.")] + [MaxLength(100, ErrorMessage = "The title shouldn't have more than 100 characters.")] + public string Title { get; set; } + + [MaxLength(1500, ErrorMessage = "The description shouldn't have more than 1500 characters.")] + public virtual string Description { get; set; } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Models/CourseForUpdateDto.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Models/CourseForUpdateDto.cs new file mode 100644 index 0000000..827ea62 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Models/CourseForUpdateDto.cs @@ -0,0 +1,16 @@ +using CourseLibrary.API.ValidationAttributes; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace CourseLibrary.API.Models +{ + public class CourseForUpdateDto : CourseForManipulationDto + { + [Required(ErrorMessage = "You should fill out a description.")] + public override string Description { get => base.Description; set => base.Description = value; } + + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Profiles/AuthorsProfile.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Profiles/AuthorsProfile.cs new file mode 100644 index 0000000..18006c3 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Profiles/AuthorsProfile.cs @@ -0,0 +1,25 @@ +using AutoMapper; +using CourseLibrary.API.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace CourseLibrary.API.Profiles +{ + public class AuthorsProfile : Profile + { + public AuthorsProfile() + { + CreateMap() + .ForMember( + dest => dest.Name, + opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}")) + .ForMember( + dest => dest.Age, + opt => opt.MapFrom(src => src.DateOfBirth.GetCurrentAge())); + + CreateMap(); + } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Profiles/CoursesProfile.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Profiles/CoursesProfile.cs new file mode 100644 index 0000000..da41353 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Profiles/CoursesProfile.cs @@ -0,0 +1,19 @@ +using AutoMapper; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace CourseLibrary.API.Profiles +{ + public class CoursesProfile : Profile + { + public CoursesProfile() + { + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Program.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Program.cs new file mode 100644 index 0000000..f896dc6 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Program.cs @@ -0,0 +1,48 @@ +using CourseLibrary.API.DbContexts; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; + +namespace CourseLibrary.API +{ + public class Program + { + + public static void Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + + // migrate the database. Best practice = in Main, using service scope + using (var scope = host.Services.CreateScope()) + { + try + { + var context = scope.ServiceProvider.GetService(); + // for demo purposes, delete the database & migrate on startup so + // we can start with a clean slate + context.Database.EnsureDeleted(); + context.Database.Migrate(); + } + catch (Exception ex) + { + var logger = scope.ServiceProvider.GetRequiredService>(); + logger.LogError(ex, "An error occurred while migrating the database."); + } + } + + // run the web app + host.Run(); + } + + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Properties/launchSettings.json b/Finished sample/CourseLibrary/CourseLibrary.API/Properties/launchSettings.json new file mode 100644 index 0000000..bf9e40d --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Properties/launchSettings.json @@ -0,0 +1,33 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iis": { + "applicationUrl": "http://localhost/CourseLibrary.API", + "sslPort": 0 + }, + "iisExpress": { + "applicationUrl": "http://localhost:51044", + "sslPort": 0 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:51044" + }, + "CourseLibrary.API": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/ResourceParameters/AuthorsResourceParameters.cs b/Finished sample/CourseLibrary/CourseLibrary.API/ResourceParameters/AuthorsResourceParameters.cs new file mode 100644 index 0000000..c1be8dc --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/ResourceParameters/AuthorsResourceParameters.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace CourseLibrary.API.ResourceParameters +{ + public class AuthorsResourceParameters + { + public string MainCategory { get; set; } + public string SearchQuery { get; set; } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Services/CourseLibraryRepository.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Services/CourseLibraryRepository.cs new file mode 100644 index 0000000..1f7c844 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Services/CourseLibraryRepository.cs @@ -0,0 +1,196 @@ +using CourseLibrary.API.DbContexts; +using CourseLibrary.API.Entities; +using CourseLibrary.API.ResourceParameters; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CourseLibrary.API.Services +{ + public class CourseLibraryRepository : ICourseLibraryRepository, IDisposable + { + private readonly CourseLibraryContext _context; + + public CourseLibraryRepository(CourseLibraryContext context ) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public void AddCourse(Guid authorId, Course course) + { + if (authorId == Guid.Empty) + { + throw new ArgumentNullException(nameof(authorId)); + } + + if (course == null) + { + throw new ArgumentNullException(nameof(course)); + } + // always set the AuthorId to the passed-in authorId + course.AuthorId = authorId; + _context.Courses.Add(course); + } + + public void DeleteCourse(Course course) + { + _context.Courses.Remove(course); + } + + public Course GetCourse(Guid authorId, Guid courseId) + { + if (authorId == Guid.Empty) + { + throw new ArgumentNullException(nameof(authorId)); + } + + if (courseId == Guid.Empty) + { + throw new ArgumentNullException(nameof(courseId)); + } + + return _context.Courses + .Where(c => c.AuthorId == authorId && c.Id == courseId).FirstOrDefault(); + } + + public IEnumerable GetCourses(Guid authorId) + { + if (authorId == Guid.Empty) + { + throw new ArgumentNullException(nameof(authorId)); + } + + return _context.Courses + .Where(c => c.AuthorId == authorId) + .OrderBy(c => c.Title).ToList(); + } + + public void UpdateCourse(Course course) + { + // no code in this implementation + } + + public void AddAuthor(Author author) + { + if (author == null) + { + throw new ArgumentNullException(nameof(author)); + } + + // the repository fills the id (instead of using identity columns) + author.Id = Guid.NewGuid(); + + foreach (var course in author.Courses) + { + course.Id = Guid.NewGuid(); + } + + _context.Authors.Add(author); + } + + public bool AuthorExists(Guid authorId) + { + if (authorId == Guid.Empty) + { + throw new ArgumentNullException(nameof(authorId)); + } + + return _context.Authors.Any(a => a.Id == authorId); + } + + public void DeleteAuthor(Author author) + { + if (author == null) + { + throw new ArgumentNullException(nameof(author)); + } + + _context.Authors.Remove(author); + } + + public Author GetAuthor(Guid authorId) + { + if (authorId == Guid.Empty) + { + throw new ArgumentNullException(nameof(authorId)); + } + + return _context.Authors.FirstOrDefault(a => a.Id == authorId); + } + + public IEnumerable GetAuthors() + { + return _context.Authors.ToList(); + } + + public IEnumerable GetAuthors(AuthorsResourceParameters authorsResourceParameters) + { + if (authorsResourceParameters == null) + { + throw new ArgumentNullException(nameof(authorsResourceParameters)); + } + + if (string.IsNullOrWhiteSpace(authorsResourceParameters.MainCategory) + && string.IsNullOrWhiteSpace(authorsResourceParameters.SearchQuery)) + { + return GetAuthors(); + } + + var collection = _context.Authors as IQueryable; + + if (!string.IsNullOrWhiteSpace(authorsResourceParameters.MainCategory)) + { + var mainCategory = authorsResourceParameters.MainCategory.Trim(); + collection = collection.Where(a => a.MainCategory == mainCategory); + } + + if (!string.IsNullOrWhiteSpace(authorsResourceParameters.SearchQuery)) + { + + var searchQuery = authorsResourceParameters.SearchQuery.Trim(); + collection = collection.Where(a => a.MainCategory.Contains(searchQuery) + || a.FirstName.Contains(searchQuery) + || a.LastName.Contains(searchQuery)); + } + + return collection.ToList(); + } + + public IEnumerable GetAuthors(IEnumerable authorIds) + { + if (authorIds == null) + { + throw new ArgumentNullException(nameof(authorIds)); + } + + return _context.Authors.Where(a => authorIds.Contains(a.Id)) + .OrderBy(a => a.FirstName) + .OrderBy(a => a.LastName) + .ToList(); + } + + public void UpdateAuthor(Author author) + { + // no code in this implementation + } + + public bool Save() + { + return (_context.SaveChanges() >= 0); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // dispose resources when needed + } + } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Services/ICourseLibraryRepository.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Services/ICourseLibraryRepository.cs new file mode 100644 index 0000000..641fc46 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Services/ICourseLibraryRepository.cs @@ -0,0 +1,25 @@ +using CourseLibrary.API.Entities; +using CourseLibrary.API.ResourceParameters; +using System; +using System.Collections.Generic; + +namespace CourseLibrary.API.Services +{ + public interface ICourseLibraryRepository + { + IEnumerable GetCourses(Guid authorId); + Course GetCourse(Guid authorId, Guid courseId); + void AddCourse(Guid authorId, Course course); + void UpdateCourse(Course course); + void DeleteCourse(Course course); + IEnumerable GetAuthors(); + IEnumerable GetAuthors(AuthorsResourceParameters authorsResourceParameters); + Author GetAuthor(Guid authorId); + IEnumerable GetAuthors(IEnumerable authorIds); + void AddAuthor(Author author); + void DeleteAuthor(Author author); + void UpdateAuthor(Author author); + bool AuthorExists(Guid authorId); + bool Save(); + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/Startup.cs b/Finished sample/CourseLibrary/CourseLibrary.API/Startup.cs new file mode 100644 index 0000000..3481444 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/Startup.cs @@ -0,0 +1,102 @@ +using AutoMapper; +using CourseLibrary.API.DbContexts; +using CourseLibrary.API.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Newtonsoft.Json.Serialization; +using System; + +namespace CourseLibrary.API +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(setupAction => + { + setupAction.ReturnHttpNotAcceptable = true; + + }).AddNewtonsoftJson(setupAction => + { + setupAction.SerializerSettings.ContractResolver = + new CamelCasePropertyNamesContractResolver(); + }) + .AddXmlDataContractSerializerFormatters() + .ConfigureApiBehaviorOptions(setupAction => + { + setupAction.InvalidModelStateResponseFactory = context => + { + var problemDetails = new ValidationProblemDetails(context.ModelState) + { + Type = "https://courselibrary.com/modelvalidationproblem", + Title = "One or more model validation errors occurred.", + Status = StatusCodes.Status422UnprocessableEntity, + Detail = "See the errors property for details.", + Instance = context.HttpContext.Request.Path + }; + + problemDetails.Extensions.Add("traceId", context.HttpContext.TraceIdentifier); + + return new UnprocessableEntityObjectResult(problemDetails) + { + ContentTypes = { "application/problem+json" } + }; + }; + }); + + services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); + + services.AddScoped(); + + services.AddDbContext(options => + { + options.UseSqlServer( + @"Server=(localdb)\mssqllocaldb;Database=CourseLibraryDB;Trusted_Connection=True;"); + }); + } + + // 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(); + } + else + { + app.UseExceptionHandler(appBuilder => + { + appBuilder.Run(async context => + { + context.Response.StatusCode = 500; + await context.Response.WriteAsync("An unexpected fault happened. Try again later."); + }); + }); + + } + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/ValidationAttributes/CourseTitleMustBeDifferentFromDescriptionAttribute.cs b/Finished sample/CourseLibrary/CourseLibrary.API/ValidationAttributes/CourseTitleMustBeDifferentFromDescriptionAttribute.cs new file mode 100644 index 0000000..7697110 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/ValidationAttributes/CourseTitleMustBeDifferentFromDescriptionAttribute.cs @@ -0,0 +1,26 @@ +using CourseLibrary.API.Models; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace CourseLibrary.API.ValidationAttributes +{ + public class CourseTitleMustBeDifferentFromDescriptionAttribute : ValidationAttribute + { + protected override ValidationResult IsValid(object value, + ValidationContext validationContext) + { + var course = (CourseForManipulationDto)validationContext.ObjectInstance; + + if (course.Title == course.Description) + { + return new ValidationResult(ErrorMessage, + new[] { nameof(CourseForManipulationDto) }); + } + + return ValidationResult.Success; + } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/appsettings.Development.json b/Finished sample/CourseLibrary/CourseLibrary.API/appsettings.Development.json new file mode 100644 index 0000000..e203e94 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.API/appsettings.json b/Finished sample/CourseLibrary/CourseLibrary.API/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.API/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/Finished sample/CourseLibrary/CourseLibrary.sln b/Finished sample/CourseLibrary/CourseLibrary.sln new file mode 100644 index 0000000..719c756 --- /dev/null +++ b/Finished sample/CourseLibrary/CourseLibrary.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29009.5 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CourseLibrary.API", "CourseLibrary.API\CourseLibrary.API.csproj", "{2900C008-F2EC-4A0D-8074-F47EB9F84036}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2900C008-F2EC-4A0D-8074-F47EB9F84036}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2900C008-F2EC-4A0D-8074-F47EB9F84036}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2900C008-F2EC-4A0D-8074-F47EB9F84036}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2900C008-F2EC-4A0D-8074-F47EB9F84036}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {08E0F198-B49E-42B9-9DB8-20DD2062C0CA} + EndGlobalSection +EndGlobal diff --git a/Starter files/DbContexts/CourseLibraryContext.cs b/Starter files/DbContexts/CourseLibraryContext.cs new file mode 100644 index 0000000..9adc91c --- /dev/null +++ b/Starter files/DbContexts/CourseLibraryContext.cs @@ -0,0 +1,113 @@ +using CourseLibrary.API.Entities; +using Microsoft.EntityFrameworkCore; +using System; + +namespace CourseLibrary.API.DbContexts +{ + public class CourseLibraryContext : DbContext + { + public CourseLibraryContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Authors { get; set; } + public DbSet Courses { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // seed the database with dummy data + modelBuilder.Entity().HasData( + new Author() + { + Id = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), + FirstName = "Berry", + LastName = "Griffin Beak Eldritch", + DateOfBirth = new DateTime(1650, 7, 23), + MainCategory = "Ships" + }, + new Author() + { + Id = Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), + FirstName = "Nancy", + LastName = "Swashbuckler Rye", + DateOfBirth = new DateTime(1668, 5, 21), + MainCategory = "Rum" + }, + new Author() + { + Id = Guid.Parse("2902b665-1190-4c70-9915-b9c2d7680450"), + FirstName = "Eli", + LastName = "Ivory Bones Sweet", + DateOfBirth = new DateTime(1701, 12, 16), + MainCategory = "Singing" + }, + new Author() + { + Id = Guid.Parse("102b566b-ba1f-404c-b2df-e2cde39ade09"), + FirstName = "Arnold", + LastName = "The Unseen Stafford", + DateOfBirth = new DateTime(1702, 3, 6), + MainCategory = "Singing" + }, + new Author() + { + Id = Guid.Parse("5b3621c0-7b12-4e80-9c8b-3398cba7ee05"), + FirstName = "Seabury", + LastName = "Toxic Reyson", + DateOfBirth = new DateTime(1690, 11, 23), + MainCategory = "Maps" + }, + new Author() + { + Id = Guid.Parse("2aadd2df-7caf-45ab-9355-7f6332985a87"), + FirstName = "Rutherford", + LastName = "Fearless Cloven", + DateOfBirth = new DateTime(1723, 4, 5), + MainCategory = "General debauchery" + }, + new Author() + { + Id = Guid.Parse("2ee49fe3-edf2-4f91-8409-3eb25ce6ca51"), + FirstName = "Atherton", + LastName = "Crow Ridley", + DateOfBirth = new DateTime(1721, 10, 11), + MainCategory = "Rum" + } + ); + + modelBuilder.Entity().HasData( + new Course + { + Id = Guid.Parse("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), + AuthorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), + Title = "Commandeering a Ship Without Getting Caught", + Description = "Commandeering a ship in rough waters isn't easy. Commandeering it without getting caught is even harder. In this course you'll learn how to sail away and avoid those pesky musketeers." + }, + new Course + { + Id = Guid.Parse("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), + AuthorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), + Title = "Overthrowing Mutiny", + Description = "In this course, the author provides tips to avoid, or, if needed, overthrow pirate mutiny." + }, + new Course + { + Id = Guid.Parse("d173e20d-159e-4127-9ce9-b0ac2564ad97"), + AuthorId = Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), + Title = "Avoiding Brawls While Drinking as Much Rum as You Desire", + Description = "Every good pirate loves rum, but it also has a tendency to get you into trouble. In this course you'll learn how to avoid that. This new exclusive edition includes an additional chapter on how to run fast without falling while drunk." + }, + new Course + { + Id = Guid.Parse("40ff5488-fdab-45b5-bc3a-14302d59869a"), + AuthorId = Guid.Parse("2902b665-1190-4c70-9915-b9c2d7680450"), + Title = "Singalong Pirate Hits", + Description = "In this course you'll learn how to sing all-time favourite pirate songs without sounding like you actually know the words or how to hold a note." + } + ); + + base.OnModelCreating(modelBuilder); + } + } +} diff --git a/Starter files/Entities/Author.cs b/Starter files/Entities/Author.cs new file mode 100644 index 0000000..5f1d9ef --- /dev/null +++ b/Starter files/Entities/Author.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace CourseLibrary.API.Entities +{ + public class Author + { + [Key] + public Guid Id { get; set; } + + [Required] + [MaxLength(50)] + public string FirstName { get; set; } + + [Required] + [MaxLength(50)] + public string LastName { get; set; } + + [Required] + public DateTimeOffset DateOfBirth { get; set; } + + [Required] + [MaxLength(50)] + public string MainCategory { get; set; } + + public ICollection Courses { get; set; } + = new List(); + } +} diff --git a/Starter files/Entities/Course.cs b/Starter files/Entities/Course.cs new file mode 100644 index 0000000..7629a75 --- /dev/null +++ b/Starter files/Entities/Course.cs @@ -0,0 +1,24 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace CourseLibrary.API.Entities +{ + public class Course + { + [Key] + public Guid Id { get; set; } + + [Required] + [MaxLength(100)] + public string Title { get; set; } + + [MaxLength(1500)] + public string Description { get; set; } + + [ForeignKey("AuthorId")] + public Author Author { get; set; } + + public Guid AuthorId { get; set; } + } +} diff --git a/Starter files/Program.cs b/Starter files/Program.cs new file mode 100644 index 0000000..f896dc6 --- /dev/null +++ b/Starter files/Program.cs @@ -0,0 +1,48 @@ +using CourseLibrary.API.DbContexts; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; + +namespace CourseLibrary.API +{ + public class Program + { + + public static void Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + + // migrate the database. Best practice = in Main, using service scope + using (var scope = host.Services.CreateScope()) + { + try + { + var context = scope.ServiceProvider.GetService(); + // for demo purposes, delete the database & migrate on startup so + // we can start with a clean slate + context.Database.EnsureDeleted(); + context.Database.Migrate(); + } + catch (Exception ex) + { + var logger = scope.ServiceProvider.GetRequiredService>(); + logger.LogError(ex, "An error occurred while migrating the database."); + } + } + + // run the web app + host.Run(); + } + + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/Starter files/Services/CourseLibraryRepository.cs b/Starter files/Services/CourseLibraryRepository.cs new file mode 100644 index 0000000..72853fb --- /dev/null +++ b/Starter files/Services/CourseLibraryRepository.cs @@ -0,0 +1,162 @@ +using CourseLibrary.API.DbContexts; +using CourseLibrary.API.Entities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CourseLibrary.API.Services +{ + public class CourseLibraryRepository : ICourseLibraryRepository, IDisposable + { + private readonly CourseLibraryContext _context; + + public CourseLibraryRepository(CourseLibraryContext context ) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public void AddCourse(Guid authorId, Course course) + { + if (authorId == Guid.Empty) + { + throw new ArgumentNullException(nameof(authorId)); + } + + if (course == null) + { + throw new ArgumentNullException(nameof(course)); + } + // always set the AuthorId to the passed-in authorId + course.AuthorId = authorId; + _context.Courses.Add(course); + } + + public void DeleteCourse(Course course) + { + _context.Courses.Remove(course); + } + + public Course GetCourse(Guid authorId, Guid courseId) + { + if (authorId == Guid.Empty) + { + throw new ArgumentNullException(nameof(authorId)); + } + + if (courseId == Guid.Empty) + { + throw new ArgumentNullException(nameof(courseId)); + } + + return _context.Courses + .Where(c => c.AuthorId == authorId && c.Id == courseId).FirstOrDefault(); + } + + public IEnumerable GetCourses(Guid authorId) + { + if (authorId == Guid.Empty) + { + throw new ArgumentNullException(nameof(authorId)); + } + + return _context.Courses + .Where(c => c.AuthorId == authorId) + .OrderBy(c => c.Title).ToList(); + } + + public void UpdateCourse(Course course) + { + // no code in this implementation + } + + public void AddAuthor(Author author) + { + if (author == null) + { + throw new ArgumentNullException(nameof(author)); + } + + // the repository fills the id (instead of using identity columns) + author.Id = Guid.NewGuid(); + + foreach (var course in author.Courses) + { + course.Id = Guid.NewGuid(); + } + + _context.Authors.Add(author); + } + + public bool AuthorExists(Guid authorId) + { + if (authorId == Guid.Empty) + { + throw new ArgumentNullException(nameof(authorId)); + } + + return _context.Authors.Any(a => a.Id == authorId); + } + + public void DeleteAuthor(Author author) + { + if (author == null) + { + throw new ArgumentNullException(nameof(author)); + } + + _context.Authors.Remove(author); + } + + public Author GetAuthor(Guid authorId) + { + if (authorId == Guid.Empty) + { + throw new ArgumentNullException(nameof(authorId)); + } + + return _context.Authors.FirstOrDefault(a => a.Id == authorId); + } + + public IEnumerable GetAuthors() + { + return _context.Authors.ToList(); + } + + public IEnumerable GetAuthors(IEnumerable authorIds) + { + if (authorIds == null) + { + throw new ArgumentNullException(nameof(authorIds)); + } + + return _context.Authors.Where(a => authorIds.Contains(a.Id)) + .OrderBy(a => a.FirstName) + .OrderBy(a => a.LastName) + .ToList(); + } + + public void UpdateAuthor(Author author) + { + // no code in this implementation + } + + public bool Save() + { + return (_context.SaveChanges() >= 0); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // dispose resources when needed + } + } + } +} diff --git a/Starter files/Services/ICourseLibraryRepository.cs b/Starter files/Services/ICourseLibraryRepository.cs new file mode 100644 index 0000000..7e58974 --- /dev/null +++ b/Starter files/Services/ICourseLibraryRepository.cs @@ -0,0 +1,23 @@ +using CourseLibrary.API.Entities; +using System; +using System.Collections.Generic; + +namespace CourseLibrary.API.Services +{ + public interface ICourseLibraryRepository + { + IEnumerable GetCourses(Guid authorId); + Course GetCourse(Guid authorId, Guid courseId); + void AddCourse(Guid authorId, Course course); + void UpdateCourse(Course course); + void DeleteCourse(Course course); + IEnumerable GetAuthors(); + Author GetAuthor(Guid authorId); + IEnumerable GetAuthors(IEnumerable authorIds); + void AddAuthor(Author author); + void DeleteAuthor(Author author); + void UpdateAuthor(Author author); + bool AuthorExists(Guid authorId); + bool Save(); + } +} diff --git a/Starter files/Startup.cs b/Starter files/Startup.cs new file mode 100644 index 0000000..569082c --- /dev/null +++ b/Starter files/Startup.cs @@ -0,0 +1,58 @@ +using CourseLibrary.API.DbContexts; +using CourseLibrary.API.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace CourseLibrary.API +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(setupAction => + { + setupAction.ReturnHttpNotAcceptable = true; + + }).AddXmlDataContractSerializerFormatters(); + + services.AddScoped(); + + services.AddDbContext(options => + { + options.UseSqlServer( + @"Server=(localdb)\mssqllocaldb;Database=CourseLibraryDB;Trusted_Connection=True;"); + }); + } + + // 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.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +}