├── .gitignore ├── Finished sample └── CourseLibrary │ ├── CourseLibrary.API │ ├── ActionConstraints │ │ └── RequestHeaderMatchesMediaTypeAttribute.cs │ ├── Controllers │ │ ├── AuthorCollectionsController.cs │ │ ├── AuthorsController.cs │ │ ├── CoursesController.cs │ │ └── RootController.cs │ ├── CourseLibrary.API.csproj │ ├── DbContexts │ │ └── CourseLibraryContext.cs │ ├── Entities │ │ ├── Author.cs │ │ └── Course.cs │ ├── Helpers │ │ ├── ArrayModelBinder.cs │ │ ├── DateTimeOffsetExtensions.cs │ │ ├── IEnumerableExtensions.cs │ │ ├── IQueryableExtensions.cs │ │ ├── ObjectExtensions.cs │ │ ├── PagedList.cs │ │ └── ResourceUriType.cs │ ├── Migrations │ │ ├── AddDateOfDeathToAuthor.Designer.cs │ │ ├── AddDateOfDeathToAuthor.cs │ │ ├── CourseLibraryContextModelSnapshot.cs │ │ ├── InitialMigration.Designer.cs │ │ └── InitialMigration.cs │ ├── Models │ │ ├── AuthorDto.cs │ │ ├── AuthorForCreationDto.cs │ │ ├── AuthorForCreationWithDateOfDeathDto.cs │ │ ├── AuthorFullDto.cs │ │ ├── CourseDto.cs │ │ ├── CourseForCreationDto.cs │ │ ├── CourseForManipulationDto.cs │ │ ├── CourseForUpdateDto.cs │ │ └── LinkDto.cs │ ├── Profiles │ │ ├── AuthorsProfile.cs │ │ └── CoursesProfile.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── ResourceParameters │ │ └── AuthorsResourceParameters.cs │ ├── Services │ │ ├── CourseLibraryRepository.cs │ │ ├── ICourseLibraryRepository.cs │ │ ├── IPropertyCheckerService.cs │ │ ├── IPropertyMapping.cs │ │ ├── IPropertyMappingService.cs │ │ ├── PropertyCheckerService.cs │ │ ├── PropertyMapping.cs │ │ ├── PropertyMappingService.cs │ │ └── PropertyMappingValue.cs │ ├── Startup.cs │ ├── ValidationAttributes │ │ └── CourseTitleMustBeDifferentFromDescriptionAttribute.cs │ ├── appsettings.Development.json │ └── appsettings.json │ └── CourseLibrary.sln ├── Implementing_Advanced_RESTful_Concerns.postman_collection ├── LICENSE ├── README.md └── Starter files └── CourseLibrary ├── CourseLibrary.API ├── Controllers │ ├── AuthorCollectionsController.cs │ ├── AuthorsController.cs │ └── CoursesController.cs ├── CourseLibrary.API.csproj ├── DbContexts │ └── CourseLibraryContext.cs ├── Entities │ ├── Author.cs │ └── Course.cs ├── Helpers │ ├── ArrayModelBinder.cs │ └── DateTimeOffsetExtensions.cs ├── Migrations │ ├── CourseLibraryContextModelSnapshot.cs │ ├── InitialMigration.Designer.cs │ └── InitialMigration.cs ├── Models │ ├── AuthorDto.cs │ ├── AuthorForCreationDto.cs │ ├── CourseDto.cs │ ├── CourseForCreationDto.cs │ ├── CourseForManipulationDto.cs │ └── CourseForUpdateDto.cs ├── Profiles │ ├── AuthorsProfile.cs │ └── CoursesProfile.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── ResourceParameters │ └── AuthorsResourceParameters.cs ├── Services │ ├── CourseLibraryRepository.cs │ └── ICourseLibraryRepository.cs ├── Startup.cs ├── ValidationAttributes │ └── CourseTitleMustBeDifferentFromDescriptionAttribute.cs ├── appsettings.Development.json └── appsettings.json └── CourseLibrary.sln /.gitignore: -------------------------------------------------------------------------------- 1 |  2 | # Created by https://www.gitignore.io/api/aspnetcore 3 | 4 | ### ASPNETCore ### 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | 8 | # User-specific files 9 | *.suo 10 | *.user 11 | *.userosscache 12 | *.sln.docstates 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Dd]ebugPublic/ 20 | [Rr]elease/ 21 | [Rr]eleases/ 22 | x64/ 23 | x86/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | [Ll]og/ 28 | 29 | # Visual Studio 2015 cache/options directory 30 | .vs/ 31 | # Uncomment if you have tasks that create the project's static files in wwwroot 32 | #wwwroot/ 33 | 34 | # MSTest test Results 35 | [Tt]est[Rr]esult*/ 36 | [Bb]uild[Ll]og.* 37 | 38 | # NUNIT 39 | *.VisualState.xml 40 | TestResult.xml 41 | 42 | # Build Results of an ATL Project 43 | [Dd]ebugPS/ 44 | [Rr]eleasePS/ 45 | dlldata.c 46 | 47 | # DNX 48 | project.lock.json 49 | project.fragment.lock.json 50 | artifacts/ 51 | 52 | *_i.c 53 | *_p.c 54 | *_i.h 55 | *.ilk 56 | *.meta 57 | *.obj 58 | *.pch 59 | *.pdb 60 | *.pgc 61 | *.pgd 62 | *.rsp 63 | *.sbr 64 | *.tlb 65 | *.tli 66 | *.tlh 67 | *.tmp 68 | *.tmp_proj 69 | *.log 70 | *.vspscc 71 | *.vssscc 72 | .builds 73 | *.pidb 74 | *.svclog 75 | *.scc 76 | 77 | # Chutzpah Test files 78 | _Chutzpah* 79 | 80 | # Visual C++ cache files 81 | ipch/ 82 | *.aps 83 | *.ncb 84 | *.opendb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | *.VC.db 89 | *.VC.VC.opendb 90 | 91 | # Visual Studio profiler 92 | *.psess 93 | *.vsp 94 | *.vspx 95 | *.sap 96 | 97 | # TFS 2012 Local Workspace 98 | $tf/ 99 | 100 | # Guidance Automation Toolkit 101 | *.gpState 102 | 103 | # ReSharper is a .NET coding add-in 104 | _ReSharper*/ 105 | *.[Rr]e[Ss]harper 106 | *.DotSettings.user 107 | 108 | # JustCode is a .NET coding add-in 109 | .JustCode 110 | 111 | # TeamCity is a build add-in 112 | _TeamCity* 113 | 114 | # DotCover is a Code Coverage Tool 115 | *.dotCover 116 | 117 | # Visual Studio code coverage results 118 | *.coverage 119 | *.coveragexml 120 | 121 | # NCrunch 122 | _NCrunch_* 123 | .*crunch*.local.xml 124 | nCrunchTemp_* 125 | 126 | # MightyMoose 127 | *.mm.* 128 | AutoTest.Net/ 129 | 130 | # Web workbench (sass) 131 | .sass-cache/ 132 | 133 | # Installshield output folder 134 | [Ee]xpress/ 135 | 136 | # DocProject is a documentation generator add-in 137 | DocProject/buildhelp/ 138 | DocProject/Help/*.HxT 139 | DocProject/Help/*.HxC 140 | DocProject/Help/*.hhc 141 | DocProject/Help/*.hhk 142 | DocProject/Help/*.hhp 143 | DocProject/Help/Html2 144 | DocProject/Help/html 145 | 146 | # Click-Once directory 147 | publish/ 148 | 149 | # Publish Web Output 150 | *.[Pp]ublish.xml 151 | *.azurePubxml 152 | # TODO: Comment the next line if you want to checkin your web deploy settings 153 | # but database connection strings (with potential passwords) will be unencrypted 154 | *.pubxml 155 | *.publishproj 156 | 157 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 158 | # checkin your Azure Web App publish settings, but sensitive information contained 159 | # in these scripts will be unencrypted 160 | PublishScripts/ 161 | 162 | # NuGet Packages 163 | *.nupkg 164 | # The packages folder can be ignored because of Package Restore 165 | **/packages/* 166 | # except build/, which is used as an MSBuild target. 167 | !**/packages/build/ 168 | # Uncomment if necessary however generally it will be regenerated when needed 169 | #!**/packages/repositories.config 170 | # NuGet v3's project.json files produces more ignoreable files 171 | *.nuget.props 172 | *.nuget.targets 173 | 174 | # Microsoft Azure Build Output 175 | csx/ 176 | *.build.csdef 177 | 178 | # Microsoft Azure Emulator 179 | ecf/ 180 | rcf/ 181 | 182 | # Windows Store app package directories and files 183 | AppPackages/ 184 | BundleArtifacts/ 185 | Package.StoreAssociation.xml 186 | _pkginfo.txt 187 | 188 | # Visual Studio cache files 189 | # files ending in .cache can be ignored 190 | *.[Cc]ache 191 | # but keep track of directories ending in .cache 192 | !*.[Cc]ache/ 193 | 194 | # Others 195 | ClientBin/ 196 | ~$* 197 | *~ 198 | *.dbmdl 199 | *.dbproj.schemaview 200 | *.jfm 201 | *.pfx 202 | *.publishsettings 203 | node_modules/ 204 | orleans.codegen.cs 205 | 206 | # Since there are multiple workflows, uncomment next line to ignore bower_components 207 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 208 | #bower_components/ 209 | 210 | # RIA/Silverlight projects 211 | Generated_Code/ 212 | 213 | # Backup & report files from converting an old project file 214 | # to a newer Visual Studio version. Backup files are not needed, 215 | # because we have git ;-) 216 | _UpgradeReport_Files/ 217 | Backup*/ 218 | UpgradeLog*.XML 219 | UpgradeLog*.htm 220 | 221 | # SQL Server files 222 | *.mdf 223 | *.ldf 224 | 225 | # Business Intelligence projects 226 | *.rdl.data 227 | *.bim.layout 228 | *.bim_*.settings 229 | 230 | # Microsoft Fakes 231 | FakesAssemblies/ 232 | 233 | # GhostDoc plugin setting file 234 | *.GhostDoc.xml 235 | 236 | # Node.js Tools for Visual Studio 237 | .ntvs_analysis.dat 238 | 239 | # Visual Studio 6 build log 240 | *.plg 241 | 242 | # Visual Studio 6 workspace options file 243 | *.opt 244 | 245 | # Visual Studio LightSwitch build output 246 | **/*.HTMLClient/GeneratedArtifacts 247 | **/*.DesktopClient/GeneratedArtifacts 248 | **/*.DesktopClient/ModelManifest.xml 249 | **/*.Server/GeneratedArtifacts 250 | **/*.Server/ModelManifest.xml 251 | _Pvt_Extensions 252 | 253 | # Paket dependency manager 254 | .paket/paket.exe 255 | paket-files/ 256 | 257 | # FAKE - F# Make 258 | .fake/ 259 | 260 | # JetBrains Rider 261 | .idea/ 262 | *.sln.iml 263 | 264 | # CodeRush 265 | .cr/ 266 | 267 | # Python Tools for Visual Studio (PTVS) 268 | __pycache__/ 269 | *.pyc 270 | 271 | # Cake - Uncomment if you are using it 272 | # tools/ 273 | 274 | 275 | # End of https://www.gitignore.io/api/aspnetcore -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/ActionConstraints/RequestHeaderMatchesMediaTypeAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.ActionConstraints; 2 | using Microsoft.AspNetCore.Mvc.Formatters; 3 | using Microsoft.Net.Http.Headers; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace CourseLibrary.API.ActionConstraints 10 | { 11 | [AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)] 12 | public class RequestHeaderMatchesMediaTypeAttribute : Attribute, IActionConstraint 13 | { 14 | private readonly MediaTypeCollection _mediaTypes = new MediaTypeCollection(); 15 | private readonly string _requestHeaderToMatch; 16 | 17 | public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch, 18 | string mediaType, params string[] otherMediaTypes) 19 | { 20 | _requestHeaderToMatch = requestHeaderToMatch 21 | ?? throw new ArgumentNullException(nameof(requestHeaderToMatch)); 22 | 23 | // check if the inputted media types are valid media types 24 | // and add them to the _mediaTypes collection 25 | 26 | if (MediaTypeHeaderValue.TryParse(mediaType, 27 | out MediaTypeHeaderValue parsedMediaType)) 28 | { 29 | _mediaTypes.Add(parsedMediaType); 30 | } 31 | else 32 | { 33 | throw new ArgumentException(nameof(mediaType)); 34 | } 35 | 36 | foreach (var otherMediaType in otherMediaTypes) 37 | { 38 | if (MediaTypeHeaderValue.TryParse(otherMediaType, 39 | out MediaTypeHeaderValue parsedOtherMediaType)) 40 | { 41 | _mediaTypes.Add(parsedOtherMediaType); 42 | } 43 | else 44 | { 45 | throw new ArgumentException(nameof(otherMediaTypes)); 46 | } 47 | } 48 | 49 | } 50 | 51 | public int Order => 0; 52 | 53 | public bool Accept(ActionConstraintContext context) 54 | { 55 | var requestHeaders = context.RouteContext.HttpContext.Request.Headers; 56 | if (!requestHeaders.ContainsKey(_requestHeaderToMatch)) 57 | { 58 | return false; 59 | } 60 | 61 | var parsedRequestMediaType = new MediaType(requestHeaders[_requestHeaderToMatch]); 62 | 63 | // if one of the media types matches, return true 64 | foreach (var mediaType in _mediaTypes) 65 | { 66 | var parsedMediaType = new MediaType(mediaType); 67 | if (parsedRequestMediaType.Equals(parsedMediaType)) 68 | { 69 | return true; 70 | } 71 | } 72 | return false; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Controllers/AuthorCollectionsController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CourseLibrary.API.Helpers; 3 | using CourseLibrary.API.Models; 4 | using CourseLibrary.API.Services; 5 | using Microsoft.AspNetCore.Mvc; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | 11 | namespace CourseLibrary.API.Controllers 12 | { 13 | [ApiController] 14 | [Route("api/authorcollections")] 15 | public class AuthorCollectionsController : ControllerBase 16 | { 17 | private readonly ICourseLibraryRepository _courseLibraryRepository; 18 | private readonly IMapper _mapper; 19 | 20 | public AuthorCollectionsController(ICourseLibraryRepository courseLibraryRepository, 21 | IMapper mapper) 22 | { 23 | _courseLibraryRepository = courseLibraryRepository ?? 24 | throw new ArgumentNullException(nameof(courseLibraryRepository)); 25 | _mapper = mapper ?? 26 | throw new ArgumentNullException(nameof(mapper)); 27 | } 28 | 29 | [HttpGet("({ids})", Name ="GetAuthorCollection")] 30 | public IActionResult GetAuthorCollection( 31 | [FromRoute] 32 | [ModelBinder(BinderType = typeof(ArrayModelBinder))] IEnumerable ids) 33 | { 34 | if (ids == null) 35 | { 36 | return BadRequest(); 37 | } 38 | 39 | var authorEntities = _courseLibraryRepository.GetAuthors(ids); 40 | 41 | if (ids.Count() != authorEntities.Count()) 42 | { 43 | return NotFound(); 44 | } 45 | 46 | var authorsToReturn = _mapper.Map>(authorEntities); 47 | 48 | return Ok(authorsToReturn); 49 | } 50 | 51 | 52 | [HttpPost] 53 | public ActionResult> CreateAuthorCollection( 54 | IEnumerable authorCollection) 55 | { 56 | var authorEntities = _mapper.Map>(authorCollection); 57 | foreach (var author in authorEntities) 58 | { 59 | _courseLibraryRepository.AddAuthor(author); 60 | } 61 | 62 | _courseLibraryRepository.Save(); 63 | 64 | var authorCollectionToReturn = _mapper.Map>(authorEntities); 65 | var idsAsString = string.Join(",", authorCollectionToReturn.Select(a => a.Id)); 66 | return CreatedAtRoute("GetAuthorCollection", 67 | new { ids = idsAsString }, 68 | authorCollectionToReturn); 69 | } 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Controllers/AuthorsController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CourseLibrary.API.ActionConstraints; 3 | using CourseLibrary.API.Helpers; 4 | using CourseLibrary.API.Models; 5 | using CourseLibrary.API.ResourceParameters; 6 | using CourseLibrary.API.Services; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Net.Http.Headers; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Text.Json; 13 | using System.Threading.Tasks; 14 | 15 | namespace CourseLibrary.API.Controllers 16 | { 17 | [ApiController] 18 | [Route("api/authors")] 19 | public class AuthorsController : ControllerBase 20 | { 21 | private readonly ICourseLibraryRepository _courseLibraryRepository; 22 | private readonly IMapper _mapper; 23 | private readonly IPropertyMappingService _propertyMappingService; 24 | private readonly IPropertyCheckerService _propertyCheckerService; 25 | 26 | public AuthorsController(ICourseLibraryRepository courseLibraryRepository, 27 | IMapper mapper, IPropertyMappingService propertyMappingService, 28 | IPropertyCheckerService propertyCheckerService) 29 | { 30 | _courseLibraryRepository = courseLibraryRepository ?? 31 | throw new ArgumentNullException(nameof(courseLibraryRepository)); 32 | _mapper = mapper ?? 33 | throw new ArgumentNullException(nameof(mapper)); 34 | _propertyMappingService = propertyMappingService ?? 35 | throw new ArgumentNullException(nameof(propertyMappingService)); 36 | _propertyCheckerService = propertyCheckerService ?? 37 | throw new ArgumentNullException(nameof(propertyCheckerService)); 38 | } 39 | 40 | [HttpGet(Name = "GetAuthors")] 41 | [HttpHead] 42 | public IActionResult GetAuthors( 43 | [FromQuery] AuthorsResourceParameters authorsResourceParameters) 44 | { 45 | if (!_propertyMappingService.ValidMappingExistsFor 46 | (authorsResourceParameters.OrderBy)) 47 | { 48 | return BadRequest(); 49 | } 50 | 51 | if (!_propertyCheckerService.TypeHasProperties 52 | (authorsResourceParameters.Fields)) 53 | { 54 | return BadRequest(); 55 | } 56 | 57 | var authorsFromRepo = _courseLibraryRepository.GetAuthors(authorsResourceParameters); 58 | 59 | var paginationMetadata = new 60 | { 61 | totalCount = authorsFromRepo.TotalCount, 62 | pageSize = authorsFromRepo.PageSize, 63 | currentPage = authorsFromRepo.CurrentPage, 64 | totalPages = authorsFromRepo.TotalPages 65 | }; 66 | 67 | Response.Headers.Add("X-Pagination", 68 | JsonSerializer.Serialize(paginationMetadata)); 69 | 70 | var links = CreateLinksForAuthors(authorsResourceParameters, 71 | authorsFromRepo.HasNext, 72 | authorsFromRepo.HasPrevious); 73 | 74 | var shapedAuthors = _mapper.Map>(authorsFromRepo) 75 | .ShapeData(authorsResourceParameters.Fields); 76 | 77 | var shapedAuthorsWithLinks = shapedAuthors.Select(author => 78 | { 79 | var authorAsDictionary = author as IDictionary; 80 | var authorLinks = CreateLinksForAuthor((Guid)authorAsDictionary["Id"], null); 81 | authorAsDictionary.Add("links", authorLinks); 82 | return authorAsDictionary; 83 | }); 84 | 85 | var linkedCollectionResource = new 86 | { 87 | value = shapedAuthorsWithLinks, 88 | links 89 | }; 90 | 91 | return Ok(linkedCollectionResource); 92 | } 93 | [Produces("application/json", 94 | "application/vnd.marvin.hateoas+json", 95 | "application/vnd.marvin.author.full+json", 96 | "application/vnd.marvin.author.full.hateoas+json", 97 | "application/vnd.marvin.author.friendly+json", 98 | "application/vnd.marvin.author.friendly.hateoas+json")] 99 | [HttpGet("{authorId}", Name ="GetAuthor")] 100 | public IActionResult GetAuthor(Guid authorId, string fields, 101 | [FromHeader(Name = "Accept")] string mediaType) 102 | { 103 | if (!MediaTypeHeaderValue.TryParse(mediaType, 104 | out MediaTypeHeaderValue parsedMediaType)) 105 | { 106 | return BadRequest(); 107 | } 108 | 109 | if (!_propertyCheckerService.TypeHasProperties 110 | (fields)) 111 | { 112 | return BadRequest(); 113 | } 114 | 115 | var authorFromRepo = _courseLibraryRepository.GetAuthor(authorId); 116 | 117 | if (authorFromRepo == null) 118 | { 119 | return NotFound(); 120 | } 121 | 122 | var includeLinks = parsedMediaType.SubTypeWithoutSuffix 123 | .EndsWith("hateoas", StringComparison.InvariantCultureIgnoreCase); 124 | 125 | IEnumerable links = new List(); 126 | 127 | if (includeLinks) 128 | { 129 | links = CreateLinksForAuthor(authorId, fields); 130 | } 131 | 132 | var primaryMediaType = includeLinks ? 133 | parsedMediaType.SubTypeWithoutSuffix 134 | .Substring(0, parsedMediaType.SubTypeWithoutSuffix.Length - 8) 135 | : parsedMediaType.SubTypeWithoutSuffix; 136 | 137 | // full author 138 | if (primaryMediaType == "vnd.marvin.author.full") 139 | { 140 | var fullResourceToReturn = _mapper.Map(authorFromRepo) 141 | .ShapeData(fields) as IDictionary; 142 | 143 | if (includeLinks) 144 | { 145 | fullResourceToReturn.Add("links", links); 146 | } 147 | 148 | return Ok(fullResourceToReturn); 149 | } 150 | 151 | // friendly author 152 | var friendlyResourceToReturn = _mapper.Map(authorFromRepo) 153 | .ShapeData(fields) as IDictionary; 154 | 155 | if (includeLinks) 156 | { 157 | friendlyResourceToReturn.Add("links", links); 158 | } 159 | 160 | return Ok(friendlyResourceToReturn); 161 | } 162 | 163 | [HttpPost(Name = "CreateAuthorWithDateOfDeath")] 164 | [RequestHeaderMatchesMediaType("Content-Type", 165 | "application/vnd.marvin.authorforcreationwithdateofdeath+json")] 166 | [Consumes("application/vnd.marvin.authorforcreationwithdateofdeath+json")] 167 | public IActionResult CreateAuthorWithDateOfDeath(AuthorForCreationWithDateOfDeathDto author) 168 | { 169 | var authorEntity = _mapper.Map(author); 170 | _courseLibraryRepository.AddAuthor(authorEntity); 171 | _courseLibraryRepository.Save(); 172 | 173 | var authorToReturn = _mapper.Map(authorEntity); 174 | 175 | var links = CreateLinksForAuthor(authorToReturn.Id, null); 176 | 177 | var linkedResourceToReturn = authorToReturn.ShapeData(null) 178 | as IDictionary; 179 | linkedResourceToReturn.Add("links", links); 180 | 181 | return CreatedAtRoute("GetAuthor", 182 | new { authorId = linkedResourceToReturn["Id"] }, 183 | linkedResourceToReturn); 184 | } 185 | 186 | [HttpPost(Name = "CreateAuthor")] 187 | [RequestHeaderMatchesMediaType("Content-Type", 188 | "application/json", 189 | "application/vnd.marvin.authorforcreation+json")] 190 | [Consumes( 191 | "application/json", 192 | "application/vnd.marvin.authorforcreation+json")] 193 | public ActionResult CreateAuthor(AuthorForCreationDto author) 194 | { 195 | var authorEntity = _mapper.Map(author); 196 | _courseLibraryRepository.AddAuthor(authorEntity); 197 | _courseLibraryRepository.Save(); 198 | 199 | var authorToReturn = _mapper.Map(authorEntity); 200 | 201 | var links = CreateLinksForAuthor(authorToReturn.Id, null); 202 | 203 | var linkedResourceToReturn = authorToReturn.ShapeData(null) 204 | as IDictionary; 205 | linkedResourceToReturn.Add("links", links); 206 | 207 | return CreatedAtRoute("GetAuthor", 208 | new { authorId = linkedResourceToReturn["Id"] }, 209 | linkedResourceToReturn); 210 | } 211 | 212 | [HttpOptions] 213 | public IActionResult GetAuthorsOptions() 214 | { 215 | Response.Headers.Add("Allow", "GET,OPTIONS,POST"); 216 | return Ok(); 217 | } 218 | 219 | [HttpDelete("{authorId}", Name = "DeleteAuthor")] 220 | public ActionResult DeleteAuthor(Guid authorId) 221 | { 222 | var authorFromRepo = _courseLibraryRepository.GetAuthor(authorId); 223 | 224 | if (authorFromRepo == null) 225 | { 226 | return NotFound(); 227 | } 228 | 229 | _courseLibraryRepository.DeleteAuthor(authorFromRepo); 230 | 231 | _courseLibraryRepository.Save(); 232 | 233 | return NoContent(); 234 | } 235 | 236 | private string CreateAuthorsResourceUri( 237 | AuthorsResourceParameters authorsResourceParameters, 238 | ResourceUriType type) 239 | { 240 | switch (type) 241 | { 242 | case ResourceUriType.PreviousPage: 243 | return Url.Link("GetAuthors", 244 | new 245 | { 246 | fields = authorsResourceParameters.Fields, 247 | orderBy = authorsResourceParameters.OrderBy, 248 | pageNumber = authorsResourceParameters.PageNumber - 1, 249 | pageSize = authorsResourceParameters.PageSize, 250 | mainCategory = authorsResourceParameters.MainCategory, 251 | searchQuery = authorsResourceParameters.SearchQuery 252 | }); 253 | case ResourceUriType.NextPage: 254 | return Url.Link("GetAuthors", 255 | new 256 | { 257 | fields = authorsResourceParameters.Fields, 258 | orderBy = authorsResourceParameters.OrderBy, 259 | pageNumber = authorsResourceParameters.PageNumber + 1, 260 | pageSize = authorsResourceParameters.PageSize, 261 | mainCategory = authorsResourceParameters.MainCategory, 262 | searchQuery = authorsResourceParameters.SearchQuery 263 | }); 264 | case ResourceUriType.Current: 265 | default: 266 | return Url.Link("GetAuthors", 267 | new 268 | { 269 | fields = authorsResourceParameters.Fields, 270 | orderBy = authorsResourceParameters.OrderBy, 271 | pageNumber = authorsResourceParameters.PageNumber, 272 | pageSize = authorsResourceParameters.PageSize, 273 | mainCategory = authorsResourceParameters.MainCategory, 274 | searchQuery = authorsResourceParameters.SearchQuery 275 | }); 276 | } 277 | 278 | } 279 | 280 | private IEnumerable CreateLinksForAuthor(Guid authorId, string fields) 281 | { 282 | var links = new List(); 283 | 284 | if (string.IsNullOrWhiteSpace(fields)) 285 | { 286 | links.Add( 287 | new LinkDto(Url.Link("GetAuthor", new { authorId }), 288 | "self", 289 | "GET")); 290 | } 291 | else 292 | { 293 | links.Add( 294 | new LinkDto(Url.Link("GetAuthor", new { authorId, fields }), 295 | "self", 296 | "GET")); 297 | } 298 | 299 | links.Add( 300 | new LinkDto(Url.Link("DeleteAuthor", new { authorId }), 301 | "delete_author", 302 | "DELETE")); 303 | 304 | links.Add( 305 | new LinkDto(Url.Link("CreateCourseForAuthor", new { authorId }), 306 | "create_course_for_author", 307 | "POST")); 308 | 309 | links.Add( 310 | new LinkDto(Url.Link("GetCoursesForAuthor", new { authorId }), 311 | "courses", 312 | "GET")); 313 | 314 | return links; 315 | } 316 | 317 | private IEnumerable CreateLinksForAuthors( 318 | AuthorsResourceParameters authorsResourceParameters, 319 | bool hasNext, bool hasPrevious) 320 | { 321 | var links = new List(); 322 | 323 | // self 324 | links.Add( 325 | new LinkDto(CreateAuthorsResourceUri( 326 | authorsResourceParameters, ResourceUriType.Current) 327 | , "self", "GET")); 328 | 329 | if (hasNext) 330 | { 331 | links.Add( 332 | new LinkDto(CreateAuthorsResourceUri( 333 | authorsResourceParameters, ResourceUriType.NextPage), 334 | "nextPage", "GET")); 335 | } 336 | 337 | if (hasPrevious) 338 | { 339 | links.Add( 340 | new LinkDto(CreateAuthorsResourceUri( 341 | authorsResourceParameters, ResourceUriType.PreviousPage), 342 | "previousPage", "GET")); 343 | } 344 | 345 | return links; 346 | } 347 | 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Controllers/CoursesController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CourseLibrary.API.Models; 3 | using CourseLibrary.API.Services; 4 | using Marvin.Cache.Headers; 5 | using Microsoft.AspNetCore.JsonPatch; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.Infrastructure; 8 | using Microsoft.AspNetCore.Mvc.ModelBinding; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Options; 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Linq; 14 | using System.Threading.Tasks; 15 | 16 | namespace CourseLibrary.API.Controllers 17 | { 18 | [ApiController] 19 | [Route("api/authors/{authorId}/courses")] 20 | // [ResponseCache(CacheProfileName = "240SecondsCacheProfile")] 21 | [HttpCacheExpiration(CacheLocation = CacheLocation.Public)] 22 | [HttpCacheValidation(MustRevalidate = true)] 23 | public class CoursesController : ControllerBase 24 | { 25 | private readonly ICourseLibraryRepository _courseLibraryRepository; 26 | private readonly IMapper _mapper; 27 | 28 | public CoursesController(ICourseLibraryRepository courseLibraryRepository, 29 | IMapper mapper) 30 | { 31 | _courseLibraryRepository = courseLibraryRepository ?? 32 | throw new ArgumentNullException(nameof(courseLibraryRepository)); 33 | _mapper = mapper ?? 34 | throw new ArgumentNullException(nameof(mapper)); 35 | } 36 | 37 | [HttpGet(Name = "GetCoursesForAuthor")] 38 | public ActionResult> GetCoursesForAuthor(Guid authorId) 39 | { 40 | if (!_courseLibraryRepository.AuthorExists(authorId)) 41 | { 42 | return NotFound(); 43 | } 44 | 45 | var coursesForAuthorFromRepo = _courseLibraryRepository.GetCourses(authorId); 46 | return Ok(_mapper.Map>(coursesForAuthorFromRepo)); 47 | } 48 | 49 | [HttpGet("{courseId}", Name = "GetCourseForAuthor")] 50 | // [ResponseCache(Duration = 120)] 51 | [HttpCacheExpiration(CacheLocation = CacheLocation.Public, MaxAge = 1000)] 52 | [HttpCacheValidation(MustRevalidate = false)] 53 | public ActionResult GetCourseForAuthor(Guid authorId, Guid courseId) 54 | { 55 | if (!_courseLibraryRepository.AuthorExists(authorId)) 56 | { 57 | return NotFound(); 58 | } 59 | 60 | var courseForAuthorFromRepo = _courseLibraryRepository.GetCourse(authorId, courseId); 61 | 62 | if (courseForAuthorFromRepo == null) 63 | { 64 | return NotFound(); 65 | } 66 | 67 | return Ok(_mapper.Map(courseForAuthorFromRepo)); 68 | } 69 | 70 | [HttpPost(Name = "CreateCourseForAuthor")] 71 | public ActionResult CreateCourseForAuthor( 72 | Guid authorId, CourseForCreationDto course) 73 | { 74 | if (!_courseLibraryRepository.AuthorExists(authorId)) 75 | { 76 | return NotFound(); 77 | } 78 | 79 | var courseEntity = _mapper.Map(course); 80 | _courseLibraryRepository.AddCourse(authorId, courseEntity); 81 | _courseLibraryRepository.Save(); 82 | 83 | var courseToReturn = _mapper.Map(courseEntity); 84 | return CreatedAtRoute("GetCourseForAuthor", 85 | new { authorId = authorId, courseId = courseToReturn.Id }, 86 | courseToReturn); 87 | } 88 | 89 | [HttpPut("{courseId}")] 90 | public IActionResult UpdateCourseForAuthor(Guid authorId, 91 | Guid courseId, 92 | CourseForUpdateDto course) 93 | { 94 | if (!_courseLibraryRepository.AuthorExists(authorId)) 95 | { 96 | return NotFound(); 97 | } 98 | 99 | var courseForAuthorFromRepo = _courseLibraryRepository.GetCourse(authorId, courseId); 100 | 101 | if (courseForAuthorFromRepo == null) 102 | { 103 | var courseToAdd = _mapper.Map(course); 104 | courseToAdd.Id = courseId; 105 | 106 | _courseLibraryRepository.AddCourse(authorId, courseToAdd); 107 | 108 | _courseLibraryRepository.Save(); 109 | 110 | var courseToReturn = _mapper.Map(courseToAdd); 111 | 112 | return CreatedAtRoute("GetCourseForAuthor", 113 | new { authorId, courseId = courseToReturn.Id }, 114 | courseToReturn); 115 | } 116 | 117 | // map the entity to a CourseForUpdateDto 118 | // apply the updated field values to that dto 119 | // map the CourseForUpdateDto back to an entity 120 | _mapper.Map(course, courseForAuthorFromRepo); 121 | 122 | _courseLibraryRepository.UpdateCourse(courseForAuthorFromRepo); 123 | 124 | _courseLibraryRepository.Save(); 125 | return NoContent(); 126 | } 127 | 128 | [HttpPatch("{courseId}")] 129 | public ActionResult PartiallyUpdateCourseForAuthor(Guid authorId, 130 | Guid courseId, 131 | JsonPatchDocument patchDocument) 132 | { 133 | if (!_courseLibraryRepository.AuthorExists(authorId)) 134 | { 135 | return NotFound(); 136 | } 137 | 138 | var courseForAuthorFromRepo = _courseLibraryRepository.GetCourse(authorId, courseId); 139 | 140 | if (courseForAuthorFromRepo == null) 141 | { 142 | var courseDto = new CourseForUpdateDto(); 143 | patchDocument.ApplyTo(courseDto, ModelState); 144 | 145 | if (!TryValidateModel(courseDto)) 146 | { 147 | return ValidationProblem(ModelState); 148 | } 149 | 150 | var courseToAdd = _mapper.Map(courseDto); 151 | courseToAdd.Id = courseId; 152 | 153 | _courseLibraryRepository.AddCourse(authorId, courseToAdd); 154 | _courseLibraryRepository.Save(); 155 | 156 | var courseToReturn = _mapper.Map(courseToAdd); 157 | 158 | return CreatedAtRoute("GetCourseForAuthor", 159 | new { authorId, courseId = courseToReturn.Id }, 160 | courseToReturn); 161 | } 162 | 163 | var courseToPatch = _mapper.Map(courseForAuthorFromRepo); 164 | // add validation 165 | patchDocument.ApplyTo(courseToPatch, ModelState); 166 | 167 | if (!TryValidateModel(courseToPatch)) 168 | { 169 | return ValidationProblem(ModelState); 170 | } 171 | 172 | _mapper.Map(courseToPatch, courseForAuthorFromRepo); 173 | 174 | _courseLibraryRepository.UpdateCourse(courseForAuthorFromRepo); 175 | 176 | _courseLibraryRepository.Save(); 177 | 178 | return NoContent(); 179 | } 180 | 181 | [HttpDelete("{courseId}")] 182 | public ActionResult DeleteCourseForAuthor(Guid authorId, Guid courseId) 183 | { 184 | if (!_courseLibraryRepository.AuthorExists(authorId)) 185 | { 186 | return NotFound(); 187 | } 188 | 189 | var courseForAuthorFromRepo = _courseLibraryRepository.GetCourse(authorId, courseId); 190 | 191 | if (courseForAuthorFromRepo == null) 192 | { 193 | return NotFound(); 194 | } 195 | 196 | _courseLibraryRepository.DeleteCourse(courseForAuthorFromRepo); 197 | _courseLibraryRepository.Save(); 198 | 199 | return NoContent(); 200 | } 201 | 202 | public override ActionResult ValidationProblem( 203 | [ActionResultObjectValue] ModelStateDictionary modelStateDictionary) 204 | { 205 | var options = HttpContext.RequestServices 206 | .GetRequiredService>(); 207 | return (ActionResult)options.Value.InvalidModelStateResponseFactory(ControllerContext); 208 | } 209 | } 210 | } -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Controllers/RootController.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.Models; 2 | using Microsoft.AspNetCore.Mvc; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace CourseLibrary.API.Controllers 9 | { 10 | [Route("api")] 11 | [ApiController] 12 | public class RootController : ControllerBase 13 | { 14 | [HttpGet(Name = "GetRoot")] 15 | public IActionResult GetRoot() 16 | { 17 | // create links for root 18 | var links = new List(); 19 | 20 | links.Add( 21 | new LinkDto(Url.Link("GetRoot", new { }), 22 | "self", 23 | "GET")); 24 | 25 | links.Add( 26 | new LinkDto(Url.Link("GetAuthors", new { }), 27 | "authors", 28 | "GET")); 29 | 30 | links.Add( 31 | new LinkDto(Url.Link("CreateAuthor", new { }), 32 | "create_author", 33 | "POST")); 34 | 35 | return Ok(links); 36 | 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/CourseLibrary.API.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/DbContexts/CourseLibraryContext.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using System; 4 | 5 | namespace CourseLibrary.API.DbContexts 6 | { 7 | public class CourseLibraryContext : DbContext 8 | { 9 | public CourseLibraryContext(DbContextOptions options) 10 | : base(options) 11 | { 12 | } 13 | 14 | public DbSet Authors { get; set; } 15 | public DbSet Courses { get; set; } 16 | 17 | protected override void OnModelCreating(ModelBuilder modelBuilder) 18 | { 19 | // seed the database with dummy data 20 | modelBuilder.Entity().HasData( 21 | new Author() 22 | { 23 | Id = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 24 | FirstName = "Berry", 25 | LastName = "Griffin Beak Eldritch", 26 | DateOfBirth = new DateTime(1650, 7, 23), 27 | MainCategory = "Ships" 28 | }, 29 | new Author() 30 | { 31 | Id = Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 32 | FirstName = "Nancy", 33 | LastName = "Swashbuckler Rye", 34 | DateOfBirth = new DateTime(1668, 5, 21), 35 | MainCategory = "Rum" 36 | }, 37 | new Author() 38 | { 39 | Id = Guid.Parse("2902b665-1190-4c70-9915-b9c2d7680450"), 40 | FirstName = "Eli", 41 | LastName = "Ivory Bones Sweet", 42 | DateOfBirth = new DateTime(1701, 12, 16), 43 | MainCategory = "Singing" 44 | }, 45 | new Author() 46 | { 47 | Id = Guid.Parse("102b566b-ba1f-404c-b2df-e2cde39ade09"), 48 | FirstName = "Arnold", 49 | LastName = "The Unseen Stafford", 50 | DateOfBirth = new DateTime(1702, 3, 6), 51 | MainCategory = "Singing" 52 | }, 53 | new Author() 54 | { 55 | Id = Guid.Parse("5b3621c0-7b12-4e80-9c8b-3398cba7ee05"), 56 | FirstName = "Seabury", 57 | LastName = "Toxic Reyson", 58 | DateOfBirth = new DateTime(1690, 11, 23), 59 | MainCategory = "Maps" 60 | }, 61 | new Author() 62 | { 63 | Id = Guid.Parse("2aadd2df-7caf-45ab-9355-7f6332985a87"), 64 | FirstName = "Rutherford", 65 | LastName = "Fearless Cloven", 66 | DateOfBirth = new DateTime(1723, 4, 5), 67 | MainCategory = "General debauchery" 68 | }, 69 | new Author() 70 | { 71 | Id = Guid.Parse("2ee49fe3-edf2-4f91-8409-3eb25ce6ca51"), 72 | FirstName = "Atherton", 73 | LastName = "Crow Ridley", 74 | DateOfBirth = new DateTime(1721, 10, 11), 75 | MainCategory = "Rum" 76 | } 77 | ); 78 | 79 | modelBuilder.Entity().HasData( 80 | new Course 81 | { 82 | Id = Guid.Parse("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 83 | AuthorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 84 | Title = "Commandeering a Ship Without Getting Caught", 85 | 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." 86 | }, 87 | new Course 88 | { 89 | Id = Guid.Parse("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 90 | AuthorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 91 | Title = "Overthrowing Mutiny", 92 | Description = "In this course, the author provides tips to avoid, or, if needed, overthrow pirate mutiny." 93 | }, 94 | new Course 95 | { 96 | Id = Guid.Parse("d173e20d-159e-4127-9ce9-b0ac2564ad97"), 97 | AuthorId = Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 98 | Title = "Avoiding Brawls While Drinking as Much Rum as You Desire", 99 | 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." 100 | }, 101 | new Course 102 | { 103 | Id = Guid.Parse("40ff5488-fdab-45b5-bc3a-14302d59869a"), 104 | AuthorId = Guid.Parse("2902b665-1190-4c70-9915-b9c2d7680450"), 105 | Title = "Singalong Pirate Hits", 106 | 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." 107 | } 108 | ); 109 | 110 | base.OnModelCreating(modelBuilder); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Entities/Author.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace CourseLibrary.API.Entities 6 | { 7 | public class Author 8 | { 9 | [Key] 10 | public Guid Id { get; set; } 11 | 12 | [Required] 13 | [MaxLength(50)] 14 | public string FirstName { get; set; } 15 | 16 | [Required] 17 | [MaxLength(50)] 18 | public string LastName { get; set; } 19 | 20 | [Required] 21 | public DateTimeOffset DateOfBirth { get; set; } 22 | 23 | public DateTimeOffset? DateOfDeath { get; set; } 24 | 25 | [Required] 26 | [MaxLength(50)] 27 | public string MainCategory { get; set; } 28 | 29 | public ICollection Courses { get; set; } 30 | = new List(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Entities/Course.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | namespace CourseLibrary.API.Entities 6 | { 7 | public class Course 8 | { 9 | [Key] 10 | public Guid Id { get; set; } 11 | 12 | [Required] 13 | [MaxLength(100)] 14 | public string Title { get; set; } 15 | 16 | [MaxLength(1500)] 17 | public string Description { get; set; } 18 | 19 | [ForeignKey("AuthorId")] 20 | public Author Author { get; set; } 21 | 22 | public Guid AuthorId { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Helpers/ArrayModelBinder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.ModelBinding; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Threading.Tasks; 8 | 9 | namespace CourseLibrary.API.Helpers 10 | { 11 | public class ArrayModelBinder : IModelBinder 12 | { 13 | public Task BindModelAsync(ModelBindingContext bindingContext) 14 | { 15 | // Our binder works only on enumerable types 16 | if (!bindingContext.ModelMetadata.IsEnumerableType) 17 | { 18 | bindingContext.Result = ModelBindingResult.Failed(); 19 | return Task.CompletedTask; 20 | } 21 | 22 | // Get the inputted value through the value provider 23 | var value = bindingContext.ValueProvider 24 | .GetValue(bindingContext.ModelName).ToString(); 25 | 26 | // If that value is null or whitespace, we return null 27 | if (string.IsNullOrWhiteSpace(value)) 28 | { 29 | bindingContext.Result = ModelBindingResult.Success(null); 30 | return Task.CompletedTask; 31 | } 32 | 33 | // The value isn't null or whitespace, 34 | // and the type of the model is enumerable. 35 | // Get the enumerable's type, and a converter 36 | var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0]; 37 | var converter = TypeDescriptor.GetConverter(elementType); 38 | 39 | // Convert each item in the value list to the enumerable type 40 | var values = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) 41 | .Select(x => converter.ConvertFromString(x.Trim())) 42 | .ToArray(); 43 | 44 | // Create an array of that type, and set it as the Model value 45 | var typedValues = Array.CreateInstance(elementType, values.Length); 46 | values.CopyTo(typedValues, 0); 47 | bindingContext.Model = typedValues; 48 | 49 | // return a successful result, passing in the Model 50 | bindingContext.Result = ModelBindingResult.Success(bindingContext.Model); 51 | return Task.CompletedTask; 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Helpers/DateTimeOffsetExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.Helpers 7 | { 8 | public static class DateTimeOffsetExtensions 9 | { 10 | public static int GetCurrentAge(this DateTimeOffset dateTimeOffset, 11 | DateTimeOffset? dateOfDeath) 12 | { 13 | var dateToCalculateTo = DateTime.UtcNow; 14 | 15 | if (dateOfDeath != null) 16 | { 17 | dateToCalculateTo = dateOfDeath.Value.UtcDateTime; 18 | } 19 | 20 | var age = dateToCalculateTo.Year - dateTimeOffset.Year; 21 | 22 | if (dateToCalculateTo < dateTimeOffset.AddYears(age)) 23 | { 24 | age--; 25 | } 26 | 27 | return age; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Helpers/IEnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Dynamic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Threading.Tasks; 7 | 8 | namespace CourseLibrary.API.Helpers 9 | { 10 | public static class IEnumerableExtensions 11 | { 12 | public static IEnumerable ShapeData( 13 | this IEnumerable source, 14 | string fields) 15 | { 16 | if (source == null) 17 | { 18 | throw new ArgumentNullException(nameof(source)); 19 | } 20 | 21 | // create a list to hold our ExpandoObjects 22 | var expandoObjectList = new List(); 23 | 24 | // create a list with PropertyInfo objects on TSource. Reflection is 25 | // expensive, so rather than doing it for each object in the list, we do 26 | // it once and reuse the results. After all, part of the reflection is on the 27 | // type of the object (TSource), not on the instance 28 | var propertyInfoList = new List(); 29 | 30 | if (string.IsNullOrWhiteSpace(fields)) 31 | { 32 | // all public properties should be in the ExpandoObject 33 | var propertyInfos = typeof(TSource) 34 | .GetProperties(BindingFlags.IgnoreCase 35 | | BindingFlags.Public | BindingFlags.Instance); 36 | 37 | propertyInfoList.AddRange(propertyInfos); 38 | } 39 | else 40 | { 41 | // the field are separated by ",", so we split it. 42 | var fieldsAfterSplit = fields.Split(','); 43 | 44 | foreach (var field in fieldsAfterSplit) 45 | { 46 | // trim each field, as it might contain leading 47 | // or trailing spaces. Can't trim the var in foreach, 48 | // so use another var. 49 | var propertyName = field.Trim(); 50 | 51 | // use reflection to get the property on the source object 52 | // we need to include public and instance, b/c specifying a binding 53 | // flag overwrites the already-existing binding flags. 54 | var propertyInfo = typeof(TSource) 55 | .GetProperty(propertyName, BindingFlags.IgnoreCase | 56 | BindingFlags.Public | BindingFlags.Instance); 57 | 58 | if (propertyInfo == null) 59 | { 60 | throw new Exception($"Property {propertyName} wasn't found on" + 61 | $" {typeof(TSource)}"); 62 | } 63 | 64 | // add propertyInfo to list 65 | propertyInfoList.Add(propertyInfo); 66 | } 67 | } 68 | 69 | // run through the source objects 70 | foreach (TSource sourceObject in source) 71 | { 72 | // create an ExpandoObject that will hold the 73 | // selected properties & values 74 | var dataShapedObject = new ExpandoObject(); 75 | 76 | // Get the value of each property we have to return. For that, 77 | // we run through the list 78 | foreach (var propertyInfo in propertyInfoList) 79 | { 80 | // GetValue returns the value of the property on the source object 81 | var propertyValue = propertyInfo.GetValue(sourceObject); 82 | 83 | // add the field to the ExpandoObject 84 | ((IDictionary)dataShapedObject) 85 | .Add(propertyInfo.Name, propertyValue); 86 | } 87 | 88 | // add the ExpandoObject to the list 89 | expandoObjectList.Add(dataShapedObject); 90 | } 91 | 92 | // return the list 93 | return expandoObjectList; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Helpers/IQueryableExtensions.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.Services; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Linq.Dynamic.Core; 6 | using System.Threading.Tasks; 7 | 8 | namespace CourseLibrary.API.Helpers 9 | { 10 | public static class IQueryableExtensions 11 | { 12 | public static IQueryable ApplySort(this IQueryable source, string orderBy, 13 | Dictionary mappingDictionary) 14 | { 15 | if (source == null) 16 | { 17 | throw new ArgumentNullException(nameof(source)); 18 | } 19 | 20 | if (mappingDictionary == null) 21 | { 22 | throw new ArgumentNullException(nameof(mappingDictionary)); 23 | } 24 | 25 | if (string.IsNullOrWhiteSpace(orderBy)) 26 | { 27 | return source; 28 | } 29 | 30 | var orderByString = string.Empty; 31 | 32 | // the orderBy string is separated by ",", so we split it. 33 | var orderByAfterSplit = orderBy.Split(','); 34 | 35 | // apply each orderby clause 36 | foreach (var orderByClause in orderByAfterSplit) 37 | { 38 | // trim the orderBy clause, as it might contain leading 39 | // or trailing spaces. Can't trim the var in foreach, 40 | // so use another var. 41 | var trimmedOrderByClause = orderByClause.Trim(); 42 | 43 | // if the sort option ends with with " desc", we order 44 | // descending, ortherwise ascending 45 | var orderDescending = trimmedOrderByClause.EndsWith(" desc"); 46 | 47 | // remove " asc" or " desc" from the orderBy clause, so we 48 | // get the property name to look for in the mapping dictionary 49 | var indexOfFirstSpace = trimmedOrderByClause.IndexOf(" "); 50 | var propertyName = indexOfFirstSpace == -1 ? 51 | trimmedOrderByClause : trimmedOrderByClause.Remove(indexOfFirstSpace); 52 | 53 | // find the matching property 54 | if (!mappingDictionary.ContainsKey(propertyName)) 55 | { 56 | throw new ArgumentException($"Key mapping for {propertyName} is missing"); 57 | } 58 | 59 | // get the PropertyMappingValue 60 | var propertyMappingValue = mappingDictionary[propertyName]; 61 | 62 | if (propertyMappingValue == null) 63 | { 64 | throw new ArgumentNullException("propertyMappingValue"); 65 | } 66 | 67 | // revert sort order if necessary 68 | if (propertyMappingValue.Revert) 69 | { 70 | orderDescending = !orderDescending; 71 | } 72 | 73 | // Run through the property names 74 | // so the orderby clauses are applied in the correct order 75 | foreach (var destinationProperty in 76 | propertyMappingValue.DestinationProperties) 77 | { 78 | orderByString = orderByString + 79 | (string.IsNullOrWhiteSpace(orderByString) ? string.Empty : ", ") 80 | + destinationProperty 81 | + (orderDescending ? " descending" : " ascending"); 82 | } 83 | } 84 | 85 | return source.OrderBy(orderByString); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Helpers/ObjectExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Dynamic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Threading.Tasks; 7 | 8 | namespace CourseLibrary.API.Helpers 9 | { 10 | public static class ObjectExtensions 11 | { 12 | public static ExpandoObject ShapeData(this TSource source, 13 | string fields) 14 | { 15 | if (source == null) 16 | { 17 | throw new ArgumentNullException(nameof(source)); 18 | } 19 | 20 | var dataShapedObject = new ExpandoObject(); 21 | 22 | if (string.IsNullOrWhiteSpace(fields)) 23 | { 24 | // all public properties should be in the ExpandoObject 25 | var propertyInfos = typeof(TSource) 26 | .GetProperties(BindingFlags.IgnoreCase | 27 | BindingFlags.Public | BindingFlags.Instance); 28 | 29 | foreach (var propertyInfo in propertyInfos) 30 | { 31 | // get the value of the property on the source object 32 | var propertyValue = propertyInfo.GetValue(source); 33 | 34 | // add the field to the ExpandoObject 35 | ((IDictionary)dataShapedObject) 36 | .Add(propertyInfo.Name, propertyValue); 37 | } 38 | 39 | return dataShapedObject; 40 | } 41 | 42 | // the field are separated by ",", so we split it. 43 | var fieldsAfterSplit = fields.Split(','); 44 | 45 | foreach (var field in fieldsAfterSplit) 46 | { 47 | // trim each field, as it might contain leading 48 | // or trailing spaces. Can't trim the var in foreach, 49 | // so use another var. 50 | var propertyName = field.Trim(); 51 | 52 | // use reflection to get the property on the source object 53 | // we need to include public and instance, b/c specifying a 54 | // binding flag overwrites the already-existing binding flags. 55 | var propertyInfo = typeof(TSource) 56 | .GetProperty(propertyName, 57 | BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); 58 | 59 | if (propertyInfo == null) 60 | { 61 | throw new Exception($"Property {propertyName} wasn't found " + 62 | $"on {typeof(TSource)}"); 63 | } 64 | 65 | // get the value of the property on the source object 66 | var propertyValue = propertyInfo.GetValue(source); 67 | 68 | // add the field to the ExpandoObject 69 | ((IDictionary)dataShapedObject) 70 | .Add(propertyInfo.Name, propertyValue); 71 | } 72 | 73 | // return the list 74 | return dataShapedObject; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Helpers/PagedList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.Helpers 7 | { 8 | public class PagedList: List 9 | { 10 | public int CurrentPage { get; private set; } 11 | public int TotalPages { get; private set; } 12 | public int PageSize { get; private set; } 13 | public int TotalCount { get; private set; } 14 | public bool HasPrevious => (CurrentPage > 1); 15 | public bool HasNext => (CurrentPage < TotalPages); 16 | 17 | public PagedList(List items, int count, int pageNumber, int pageSize) 18 | { 19 | TotalCount = count; 20 | PageSize = pageSize; 21 | CurrentPage = pageNumber; 22 | TotalPages = (int)Math.Ceiling(count / (double)pageSize); 23 | AddRange(items); 24 | } 25 | public static PagedList Create(IQueryable source, int pageNumber, int pageSize) 26 | { 27 | var count = source.Count(); 28 | var items = source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToList(); 29 | return new PagedList(items, count, pageNumber, pageSize); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Helpers/ResourceUriType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.Helpers 7 | { 8 | public enum ResourceUriType 9 | { 10 | PreviousPage, 11 | NextPage, 12 | Current 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Migrations/AddDateOfDeathToAuthor.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using CourseLibrary.API.DbContexts; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 9 | 10 | namespace CourseLibrary.API.Migrations 11 | { 12 | [DbContext(typeof(CourseLibraryContext))] 13 | [Migration("20190925094303_AddDateOfDeathToAuthor")] 14 | partial class AddDateOfDeathToAuthor 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "3.0.0") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("CourseLibrary.API.Entities.Author", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd() 28 | .HasColumnType("uniqueidentifier"); 29 | 30 | b.Property("DateOfBirth") 31 | .HasColumnType("datetimeoffset"); 32 | 33 | b.Property("DateOfDeath") 34 | .HasColumnType("datetimeoffset"); 35 | 36 | b.Property("FirstName") 37 | .IsRequired() 38 | .HasColumnType("nvarchar(50)") 39 | .HasMaxLength(50); 40 | 41 | b.Property("LastName") 42 | .IsRequired() 43 | .HasColumnType("nvarchar(50)") 44 | .HasMaxLength(50); 45 | 46 | b.Property("MainCategory") 47 | .IsRequired() 48 | .HasColumnType("nvarchar(50)") 49 | .HasMaxLength(50); 50 | 51 | b.HasKey("Id"); 52 | 53 | b.ToTable("Authors"); 54 | 55 | b.HasData( 56 | new 57 | { 58 | Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 59 | DateOfBirth = new DateTimeOffset(new DateTime(1650, 7, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 60 | FirstName = "Berry", 61 | LastName = "Griffin Beak Eldritch", 62 | MainCategory = "Ships" 63 | }, 64 | new 65 | { 66 | Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 67 | DateOfBirth = new DateTimeOffset(new DateTime(1668, 5, 21, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 68 | FirstName = "Nancy", 69 | LastName = "Swashbuckler Rye", 70 | MainCategory = "Rum" 71 | }, 72 | new 73 | { 74 | Id = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 75 | DateOfBirth = new DateTimeOffset(new DateTime(1701, 12, 16, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 76 | FirstName = "Eli", 77 | LastName = "Ivory Bones Sweet", 78 | MainCategory = "Singing" 79 | }, 80 | new 81 | { 82 | Id = new Guid("102b566b-ba1f-404c-b2df-e2cde39ade09"), 83 | DateOfBirth = new DateTimeOffset(new DateTime(1702, 3, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 84 | FirstName = "Arnold", 85 | LastName = "The Unseen Stafford", 86 | MainCategory = "Singing" 87 | }, 88 | new 89 | { 90 | Id = new Guid("5b3621c0-7b12-4e80-9c8b-3398cba7ee05"), 91 | DateOfBirth = new DateTimeOffset(new DateTime(1690, 11, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 92 | FirstName = "Seabury", 93 | LastName = "Toxic Reyson", 94 | MainCategory = "Maps" 95 | }, 96 | new 97 | { 98 | Id = new Guid("2aadd2df-7caf-45ab-9355-7f6332985a87"), 99 | DateOfBirth = new DateTimeOffset(new DateTime(1723, 4, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 100 | FirstName = "Rutherford", 101 | LastName = "Fearless Cloven", 102 | MainCategory = "General debauchery" 103 | }, 104 | new 105 | { 106 | Id = new Guid("2ee49fe3-edf2-4f91-8409-3eb25ce6ca51"), 107 | DateOfBirth = new DateTimeOffset(new DateTime(1721, 10, 11, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 108 | FirstName = "Atherton", 109 | LastName = "Crow Ridley", 110 | MainCategory = "Rum" 111 | }); 112 | }); 113 | 114 | modelBuilder.Entity("CourseLibrary.API.Entities.Course", b => 115 | { 116 | b.Property("Id") 117 | .ValueGeneratedOnAdd() 118 | .HasColumnType("uniqueidentifier"); 119 | 120 | b.Property("AuthorId") 121 | .HasColumnType("uniqueidentifier"); 122 | 123 | b.Property("Description") 124 | .HasColumnType("nvarchar(1500)") 125 | .HasMaxLength(1500); 126 | 127 | b.Property("Title") 128 | .IsRequired() 129 | .HasColumnType("nvarchar(100)") 130 | .HasMaxLength(100); 131 | 132 | b.HasKey("Id"); 133 | 134 | b.HasIndex("AuthorId"); 135 | 136 | b.ToTable("Courses"); 137 | 138 | b.HasData( 139 | new 140 | { 141 | Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 142 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 143 | 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.", 144 | Title = "Commandeering a Ship Without Getting Caught" 145 | }, 146 | new 147 | { 148 | Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 149 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 150 | Description = "In this course, the author provides tips to avoid, or, if needed, overthrow pirate mutiny.", 151 | Title = "Overthrowing Mutiny" 152 | }, 153 | new 154 | { 155 | Id = new Guid("d173e20d-159e-4127-9ce9-b0ac2564ad97"), 156 | AuthorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 157 | 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.", 158 | Title = "Avoiding Brawls While Drinking as Much Rum as You Desire" 159 | }, 160 | new 161 | { 162 | Id = new Guid("40ff5488-fdab-45b5-bc3a-14302d59869a"), 163 | AuthorId = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 164 | 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.", 165 | Title = "Singalong Pirate Hits" 166 | }); 167 | }); 168 | 169 | modelBuilder.Entity("CourseLibrary.API.Entities.Course", b => 170 | { 171 | b.HasOne("CourseLibrary.API.Entities.Author", "Author") 172 | .WithMany("Courses") 173 | .HasForeignKey("AuthorId") 174 | .OnDelete(DeleteBehavior.Cascade) 175 | .IsRequired(); 176 | }); 177 | #pragma warning restore 612, 618 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Migrations/AddDateOfDeathToAuthor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace CourseLibrary.API.Migrations 5 | { 6 | public partial class AddDateOfDeathToAuthor : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.AddColumn( 11 | name: "DateOfDeath", 12 | table: "Authors", 13 | nullable: true); 14 | } 15 | 16 | protected override void Down(MigrationBuilder migrationBuilder) 17 | { 18 | migrationBuilder.DropColumn( 19 | name: "DateOfDeath", 20 | table: "Authors"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Migrations/CourseLibraryContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using CourseLibrary.API.DbContexts; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace CourseLibrary.API.Migrations 10 | { 11 | [DbContext(typeof(CourseLibraryContext))] 12 | partial class CourseLibraryContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "3.0.0") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("CourseLibrary.API.Entities.Author", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd() 26 | .HasColumnType("uniqueidentifier"); 27 | 28 | b.Property("DateOfBirth") 29 | .HasColumnType("datetimeoffset"); 30 | 31 | b.Property("DateOfDeath") 32 | .HasColumnType("datetimeoffset"); 33 | 34 | b.Property("FirstName") 35 | .IsRequired() 36 | .HasColumnType("nvarchar(50)") 37 | .HasMaxLength(50); 38 | 39 | b.Property("LastName") 40 | .IsRequired() 41 | .HasColumnType("nvarchar(50)") 42 | .HasMaxLength(50); 43 | 44 | b.Property("MainCategory") 45 | .IsRequired() 46 | .HasColumnType("nvarchar(50)") 47 | .HasMaxLength(50); 48 | 49 | b.HasKey("Id"); 50 | 51 | b.ToTable("Authors"); 52 | 53 | b.HasData( 54 | new 55 | { 56 | Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 57 | DateOfBirth = new DateTimeOffset(new DateTime(1650, 7, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 58 | FirstName = "Berry", 59 | LastName = "Griffin Beak Eldritch", 60 | MainCategory = "Ships" 61 | }, 62 | new 63 | { 64 | Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 65 | DateOfBirth = new DateTimeOffset(new DateTime(1668, 5, 21, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 66 | FirstName = "Nancy", 67 | LastName = "Swashbuckler Rye", 68 | MainCategory = "Rum" 69 | }, 70 | new 71 | { 72 | Id = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 73 | DateOfBirth = new DateTimeOffset(new DateTime(1701, 12, 16, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 74 | FirstName = "Eli", 75 | LastName = "Ivory Bones Sweet", 76 | MainCategory = "Singing" 77 | }, 78 | new 79 | { 80 | Id = new Guid("102b566b-ba1f-404c-b2df-e2cde39ade09"), 81 | DateOfBirth = new DateTimeOffset(new DateTime(1702, 3, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 82 | FirstName = "Arnold", 83 | LastName = "The Unseen Stafford", 84 | MainCategory = "Singing" 85 | }, 86 | new 87 | { 88 | Id = new Guid("5b3621c0-7b12-4e80-9c8b-3398cba7ee05"), 89 | DateOfBirth = new DateTimeOffset(new DateTime(1690, 11, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 90 | FirstName = "Seabury", 91 | LastName = "Toxic Reyson", 92 | MainCategory = "Maps" 93 | }, 94 | new 95 | { 96 | Id = new Guid("2aadd2df-7caf-45ab-9355-7f6332985a87"), 97 | DateOfBirth = new DateTimeOffset(new DateTime(1723, 4, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 98 | FirstName = "Rutherford", 99 | LastName = "Fearless Cloven", 100 | MainCategory = "General debauchery" 101 | }, 102 | new 103 | { 104 | Id = new Guid("2ee49fe3-edf2-4f91-8409-3eb25ce6ca51"), 105 | DateOfBirth = new DateTimeOffset(new DateTime(1721, 10, 11, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 106 | FirstName = "Atherton", 107 | LastName = "Crow Ridley", 108 | MainCategory = "Rum" 109 | }); 110 | }); 111 | 112 | modelBuilder.Entity("CourseLibrary.API.Entities.Course", b => 113 | { 114 | b.Property("Id") 115 | .ValueGeneratedOnAdd() 116 | .HasColumnType("uniqueidentifier"); 117 | 118 | b.Property("AuthorId") 119 | .HasColumnType("uniqueidentifier"); 120 | 121 | b.Property("Description") 122 | .HasColumnType("nvarchar(1500)") 123 | .HasMaxLength(1500); 124 | 125 | b.Property("Title") 126 | .IsRequired() 127 | .HasColumnType("nvarchar(100)") 128 | .HasMaxLength(100); 129 | 130 | b.HasKey("Id"); 131 | 132 | b.HasIndex("AuthorId"); 133 | 134 | b.ToTable("Courses"); 135 | 136 | b.HasData( 137 | new 138 | { 139 | Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 140 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 141 | 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.", 142 | Title = "Commandeering a Ship Without Getting Caught" 143 | }, 144 | new 145 | { 146 | Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 147 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 148 | Description = "In this course, the author provides tips to avoid, or, if needed, overthrow pirate mutiny.", 149 | Title = "Overthrowing Mutiny" 150 | }, 151 | new 152 | { 153 | Id = new Guid("d173e20d-159e-4127-9ce9-b0ac2564ad97"), 154 | AuthorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 155 | 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.", 156 | Title = "Avoiding Brawls While Drinking as Much Rum as You Desire" 157 | }, 158 | new 159 | { 160 | Id = new Guid("40ff5488-fdab-45b5-bc3a-14302d59869a"), 161 | AuthorId = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 162 | 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.", 163 | Title = "Singalong Pirate Hits" 164 | }); 165 | }); 166 | 167 | modelBuilder.Entity("CourseLibrary.API.Entities.Course", b => 168 | { 169 | b.HasOne("CourseLibrary.API.Entities.Author", "Author") 170 | .WithMany("Courses") 171 | .HasForeignKey("AuthorId") 172 | .OnDelete(DeleteBehavior.Cascade) 173 | .IsRequired(); 174 | }); 175 | #pragma warning restore 612, 618 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Migrations/InitialMigration.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using CourseLibrary.API.DbContexts; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 9 | 10 | namespace CourseLibrary.API.Migrations 11 | { 12 | [DbContext(typeof(CourseLibraryContext))] 13 | [Migration("20190731122611_InitialMigration")] 14 | partial class InitialMigration 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "3.0.0-preview7.19362.6") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("CourseLibrary.API.Entities.Author", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd(); 28 | 29 | b.Property("DateOfBirth"); 30 | 31 | b.Property("FirstName") 32 | .IsRequired() 33 | .HasMaxLength(50); 34 | 35 | b.Property("LastName") 36 | .IsRequired() 37 | .HasMaxLength(50); 38 | 39 | b.Property("MainCategory") 40 | .IsRequired() 41 | .HasMaxLength(50); 42 | 43 | b.HasKey("Id"); 44 | 45 | b.ToTable("Authors"); 46 | 47 | b.HasData( 48 | new 49 | { 50 | Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 51 | DateOfBirth = new DateTimeOffset(new DateTime(1970, 7, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 52 | FirstName = "Berry", 53 | LastName = "Griffin Beak Eldritch", 54 | MainCategory = "Ships" 55 | }, 56 | new 57 | { 58 | Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 59 | DateOfBirth = new DateTimeOffset(new DateTime(1968, 5, 21, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 60 | FirstName = "Nancy", 61 | LastName = "Swashbuckler Rye", 62 | MainCategory = "Rum" 63 | }, 64 | new 65 | { 66 | Id = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 67 | DateOfBirth = new DateTimeOffset(new DateTime(1991, 12, 16, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 68 | FirstName = "Eli", 69 | LastName = "Ivory Bones Sweet", 70 | MainCategory = "Singing" 71 | }, 72 | new 73 | { 74 | Id = new Guid("102b566b-ba1f-404c-b2df-e2cde39ade09"), 75 | DateOfBirth = new DateTimeOffset(new DateTime(1984, 3, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 76 | FirstName = "Arnold", 77 | LastName = "The Unseen Stafford", 78 | MainCategory = "Singing" 79 | }, 80 | new 81 | { 82 | Id = new Guid("5b3621c0-7b12-4e80-9c8b-3398cba7ee05"), 83 | DateOfBirth = new DateTimeOffset(new DateTime(1990, 11, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 84 | FirstName = "Seabury", 85 | LastName = "Toxic Reyson", 86 | MainCategory = "Maps" 87 | }, 88 | new 89 | { 90 | Id = new Guid("2aadd2df-7caf-45ab-9355-7f6332985a87"), 91 | DateOfBirth = new DateTimeOffset(new DateTime(1978, 4, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 92 | FirstName = "Rutherford", 93 | LastName = "Fearless Cloven", 94 | MainCategory = "General debauchery" 95 | }, 96 | new 97 | { 98 | Id = new Guid("2ee49fe3-edf2-4f91-8409-3eb25ce6ca51"), 99 | DateOfBirth = new DateTimeOffset(new DateTime(1959, 10, 11, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 100 | FirstName = "Atherton", 101 | LastName = "Crow Ridley", 102 | MainCategory = "Rum" 103 | }, 104 | new 105 | { 106 | Id = new Guid("71838f8b-6ab3-4539-9e67-4e77b8ede1c0"), 107 | DateOfBirth = new DateTimeOffset(new DateTime(1969, 8, 11, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 108 | FirstName = "Huxford", 109 | LastName = "The Hawk Morris", 110 | MainCategory = "Maps" 111 | }, 112 | new 113 | { 114 | Id = new Guid("119f9ccb-149d-4d3c-ad4f-40100f38e918"), 115 | DateOfBirth = new DateTimeOffset(new DateTime(1972, 1, 8, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 116 | FirstName = "Dwennon", 117 | LastName = "Rigger Quye", 118 | MainCategory = "Maps" 119 | }, 120 | new 121 | { 122 | Id = new Guid("28c1db41-f104-46e6-8943-d31c0291e0e3"), 123 | DateOfBirth = new DateTimeOffset(new DateTime(1982, 5, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 124 | FirstName = "Rushford", 125 | LastName = "Subtle Asema", 126 | MainCategory = "Rum" 127 | }, 128 | new 129 | { 130 | Id = new Guid("d94a64c2-2e8f-4162-9976-0ffe03d30767"), 131 | DateOfBirth = new DateTimeOffset(new DateTime(1976, 7, 12, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 132 | FirstName = "Hagley", 133 | LastName = "Imposter Grendel", 134 | MainCategory = "Singing" 135 | }, 136 | new 137 | { 138 | Id = new Guid("380c2c6b-0d1c-4b82-9d83-3cf635a3e62b"), 139 | DateOfBirth = new DateTimeOffset(new DateTime(1977, 2, 8, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 140 | FirstName = "Mabel", 141 | LastName = "Barnacle Grendel", 142 | MainCategory = "Maps" 143 | }); 144 | }); 145 | 146 | modelBuilder.Entity("CourseLibrary.API.Entities.Course", b => 147 | { 148 | b.Property("Id") 149 | .ValueGeneratedOnAdd(); 150 | 151 | b.Property("AuthorId"); 152 | 153 | b.Property("Description") 154 | .HasMaxLength(1500); 155 | 156 | b.Property("Title") 157 | .IsRequired() 158 | .HasMaxLength(100); 159 | 160 | b.HasKey("Id"); 161 | 162 | b.HasIndex("AuthorId"); 163 | 164 | b.ToTable("Courses"); 165 | 166 | b.HasData( 167 | new 168 | { 169 | Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 170 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 171 | 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.", 172 | Title = "Commandeering a Ship Without Getting Caught" 173 | }, 174 | new 175 | { 176 | Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 177 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 178 | Description = "In this course, the author provides tips to avoid, or, if needed, overthrow pirate mutiny.", 179 | Title = "Overthrowing Mutiny" 180 | }, 181 | new 182 | { 183 | Id = new Guid("d173e20d-159e-4127-9ce9-b0ac2564ad97"), 184 | AuthorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 185 | 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.", 186 | Title = "Avoiding Brawls While Drinking as Much Rum as You Desire" 187 | }, 188 | new 189 | { 190 | Id = new Guid("40ff5488-fdab-45b5-bc3a-14302d59869a"), 191 | AuthorId = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 192 | 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.", 193 | Title = "Singalong Pirate Hits" 194 | }); 195 | }); 196 | 197 | modelBuilder.Entity("CourseLibrary.API.Entities.Course", b => 198 | { 199 | b.HasOne("CourseLibrary.API.Entities.Author", "Author") 200 | .WithMany("Courses") 201 | .HasForeignKey("AuthorId") 202 | .OnDelete(DeleteBehavior.Cascade) 203 | .IsRequired(); 204 | }); 205 | #pragma warning restore 612, 618 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Migrations/InitialMigration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace CourseLibrary.API.Migrations 5 | { 6 | public partial class InitialMigration : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "Authors", 12 | columns: table => new 13 | { 14 | Id = table.Column(nullable: false), 15 | FirstName = table.Column(maxLength: 50, nullable: false), 16 | LastName = table.Column(maxLength: 50, nullable: false), 17 | DateOfBirth = table.Column(nullable: false), 18 | MainCategory = table.Column(maxLength: 50, nullable: false) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Authors", x => x.Id); 23 | }); 24 | 25 | migrationBuilder.CreateTable( 26 | name: "Courses", 27 | columns: table => new 28 | { 29 | Id = table.Column(nullable: false), 30 | Title = table.Column(maxLength: 100, nullable: false), 31 | Description = table.Column(maxLength: 1500, nullable: true), 32 | AuthorId = table.Column(nullable: false) 33 | }, 34 | constraints: table => 35 | { 36 | table.PrimaryKey("PK_Courses", x => x.Id); 37 | table.ForeignKey( 38 | name: "FK_Courses_Authors_AuthorId", 39 | column: x => x.AuthorId, 40 | principalTable: "Authors", 41 | principalColumn: "Id", 42 | onDelete: ReferentialAction.Cascade); 43 | }); 44 | 45 | migrationBuilder.InsertData( 46 | table: "Authors", 47 | columns: new[] { "Id", "DateOfBirth", "FirstName", "LastName", "MainCategory" }, 48 | values: new object[,] 49 | { 50 | { 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" }, 51 | { 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" }, 52 | { 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" }, 53 | { 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" }, 54 | { 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" }, 55 | { 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" }, 56 | { 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" }, 57 | { 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" }, 58 | { 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" }, 59 | { 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" }, 60 | { 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" }, 61 | { 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" } 62 | }); 63 | 64 | migrationBuilder.InsertData( 65 | table: "Courses", 66 | columns: new[] { "Id", "AuthorId", "Description", "Title" }, 67 | values: new object[,] 68 | { 69 | { 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" }, 70 | { 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" }, 71 | { 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" }, 72 | { 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" } 73 | }); 74 | 75 | migrationBuilder.CreateIndex( 76 | name: "IX_Courses_AuthorId", 77 | table: "Courses", 78 | column: "AuthorId"); 79 | } 80 | 81 | protected override void Down(MigrationBuilder migrationBuilder) 82 | { 83 | migrationBuilder.DropTable( 84 | name: "Courses"); 85 | 86 | migrationBuilder.DropTable( 87 | name: "Authors"); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Models/AuthorDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.Models 7 | { 8 | public class AuthorDto 9 | { 10 | public Guid Id { get; set; } 11 | public string Name { get; set; } 12 | public int Age { get; set; } 13 | public string MainCategory { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Models/AuthorForCreationDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.Models 7 | { 8 | public class AuthorForCreationDto 9 | { 10 | public string FirstName { get; set; } 11 | public string LastName { get; set; } 12 | public DateTimeOffset DateOfBirth { get; set; } 13 | public string MainCategory { get; set; } 14 | public ICollection Courses { get; set; } 15 | = new List(); 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Models/AuthorForCreationWithDateOfDeathDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.Models 7 | { 8 | public class AuthorForCreationWithDateOfDeathDto : AuthorForCreationDto 9 | { 10 | public DateTimeOffset? DateOfDeath { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Models/AuthorFullDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.Models 7 | { 8 | public class AuthorFullDto 9 | { 10 | public Guid Id { get; set; } 11 | public string FirstName { get; set; } 12 | public string LastName { get; set; } 13 | public DateTimeOffset DateOfBirth { get; set; } 14 | public string MainCategory { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Models/CourseDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.Models 7 | { 8 | public class CourseDto 9 | { 10 | public Guid Id { get; set; } 11 | 12 | public string Title { get; set; } 13 | 14 | public string Description { get; set; } 15 | 16 | public Guid AuthorId { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Models/CourseForCreationDto.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.ValidationAttributes; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace CourseLibrary.API.Models 9 | { 10 | public class CourseForCreationDto : CourseForManipulationDto 11 | { } 12 | } 13 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Models/CourseForManipulationDto.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.ValidationAttributes; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace CourseLibrary.API.Models 9 | { 10 | [CourseTitleMustBeDifferentFromDescription( 11 | ErrorMessage = "Title must be different from description.")] 12 | public abstract class CourseForManipulationDto 13 | { 14 | [Required(ErrorMessage = "You should fill out a title.")] 15 | [MaxLength(100, ErrorMessage = "The title shouldn't have more than 100 characters.")] 16 | public string Title { get; set; } 17 | 18 | [MaxLength(1500, ErrorMessage = "The description shouldn't have more than 1500 characters.")] 19 | public virtual string Description { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Models/CourseForUpdateDto.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.ValidationAttributes; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace CourseLibrary.API.Models 9 | { 10 | public class CourseForUpdateDto : CourseForManipulationDto 11 | { 12 | [Required(ErrorMessage = "You should fill out a description.")] 13 | public override string Description { get => base.Description; set => base.Description = value; } 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Models/LinkDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.Models 7 | { 8 | public class LinkDto 9 | { 10 | public string Href { get; private set; } 11 | public string Rel { get; private set; } 12 | public string Method { get; private set; } 13 | 14 | public LinkDto(string href, string rel, string method) 15 | { 16 | Href = href; 17 | Rel = rel; 18 | Method = method; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Profiles/AuthorsProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CourseLibrary.API.Helpers; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace CourseLibrary.API.Profiles 9 | { 10 | public class AuthorsProfile : Profile 11 | { 12 | public AuthorsProfile() 13 | { 14 | CreateMap() 15 | .ForMember( 16 | dest => dest.Name, 17 | opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}")) 18 | .ForMember( 19 | dest => dest.Age, 20 | opt => opt.MapFrom(src => src.DateOfBirth.GetCurrentAge(src.DateOfDeath))); 21 | 22 | CreateMap(); 23 | 24 | CreateMap(); 25 | 26 | CreateMap(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Profiles/CoursesProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace CourseLibrary.API.Profiles 8 | { 9 | public class CoursesProfile : Profile 10 | { 11 | public CoursesProfile() 12 | { 13 | CreateMap(); 14 | CreateMap(); 15 | CreateMap(); 16 | CreateMap(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Program.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.DbContexts; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | using System; 8 | 9 | namespace CourseLibrary.API 10 | { 11 | public class Program 12 | { 13 | 14 | public static void Main(string[] args) 15 | { 16 | var host = CreateHostBuilder(args).Build(); 17 | 18 | // migrate the database. Best practice = in Main, using service scope 19 | using (var scope = host.Services.CreateScope()) 20 | { 21 | try 22 | { 23 | var context = scope.ServiceProvider.GetService(); 24 | // for demo purposes, delete the database & migrate on startup so 25 | // we can start with a clean slate 26 | context.Database.EnsureDeleted(); 27 | context.Database.Migrate(); 28 | } 29 | catch (Exception ex) 30 | { 31 | var logger = scope.ServiceProvider.GetRequiredService>(); 32 | logger.LogError(ex, "An error occurred while migrating the database."); 33 | } 34 | } 35 | 36 | // run the web app 37 | host.Run(); 38 | } 39 | 40 | 41 | public static IHostBuilder CreateHostBuilder(string[] args) => 42 | Host.CreateDefaultBuilder(args) 43 | .ConfigureWebHostDefaults(webBuilder => 44 | { 45 | webBuilder.UseStartup(); 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iis": { 6 | "applicationUrl": "http://localhost/CourseLibrary.API", 7 | "sslPort": 0 8 | }, 9 | "iisExpress": { 10 | "applicationUrl": "http://localhost:51044", 11 | "sslPort": 0 12 | } 13 | }, 14 | "$schema": "http://json.schemastore.org/launchsettings.json", 15 | "profiles": { 16 | "IIS Express": { 17 | "commandName": "Project", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | }, 21 | "applicationUrl": "http://localhost:51044" 22 | }, 23 | "CourseLibrary.API": { 24 | "commandName": "Project", 25 | "launchBrowser": true, 26 | "launchUrl": "api/values", 27 | "environmentVariables": { 28 | "ASPNETCORE_ENVIRONMENT": "Development" 29 | }, 30 | "applicationUrl": "http://localhost:5000" 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/ResourceParameters/AuthorsResourceParameters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.ResourceParameters 7 | { 8 | public class AuthorsResourceParameters 9 | { 10 | const int maxPageSize = 20; 11 | public string MainCategory { get; set; } 12 | public string SearchQuery { get; set; } 13 | public int PageNumber { get; set; } = 1; 14 | 15 | private int _pageSize = 10; 16 | public int PageSize 17 | { 18 | get => _pageSize; 19 | set => _pageSize = (value > maxPageSize) ? maxPageSize : value; 20 | } 21 | 22 | public string OrderBy { get; set; } = "Name"; 23 | public string Fields { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Services/CourseLibraryRepository.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.DbContexts; 2 | using CourseLibrary.API.Entities; 3 | using CourseLibrary.API.Helpers; 4 | using CourseLibrary.API.ResourceParameters; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace CourseLibrary.API.Services 10 | { 11 | public class CourseLibraryRepository : ICourseLibraryRepository, IDisposable 12 | { 13 | private readonly CourseLibraryContext _context; 14 | private readonly IPropertyMappingService _propertyMappingService; 15 | 16 | public CourseLibraryRepository(CourseLibraryContext context, 17 | IPropertyMappingService propertyMappingService) 18 | { 19 | _context = context ?? throw new ArgumentNullException(nameof(context)); 20 | _propertyMappingService = propertyMappingService ?? 21 | throw new ArgumentNullException(nameof(propertyMappingService)); 22 | } 23 | 24 | public void AddCourse(Guid authorId, Course course) 25 | { 26 | if (authorId == Guid.Empty) 27 | { 28 | throw new ArgumentNullException(nameof(authorId)); 29 | } 30 | 31 | if (course == null) 32 | { 33 | throw new ArgumentNullException(nameof(course)); 34 | } 35 | // always set the AuthorId to the passed-in authorId 36 | course.AuthorId = authorId; 37 | _context.Courses.Add(course); 38 | } 39 | 40 | public void DeleteCourse(Course course) 41 | { 42 | _context.Courses.Remove(course); 43 | } 44 | 45 | public Course GetCourse(Guid authorId, Guid courseId) 46 | { 47 | if (authorId == Guid.Empty) 48 | { 49 | throw new ArgumentNullException(nameof(authorId)); 50 | } 51 | 52 | if (courseId == Guid.Empty) 53 | { 54 | throw new ArgumentNullException(nameof(courseId)); 55 | } 56 | 57 | return _context.Courses 58 | .Where(c => c.AuthorId == authorId && c.Id == courseId).FirstOrDefault(); 59 | } 60 | 61 | public IEnumerable GetCourses(Guid authorId) 62 | { 63 | if (authorId == Guid.Empty) 64 | { 65 | throw new ArgumentNullException(nameof(authorId)); 66 | } 67 | 68 | return _context.Courses 69 | .Where(c => c.AuthorId == authorId) 70 | .OrderBy(c => c.Title).ToList(); 71 | } 72 | 73 | public void UpdateCourse(Course course) 74 | { 75 | // no code in this implementation 76 | } 77 | 78 | public void AddAuthor(Author author) 79 | { 80 | if (author == null) 81 | { 82 | throw new ArgumentNullException(nameof(author)); 83 | } 84 | 85 | // the repository fills the id (instead of using identity columns) 86 | author.Id = Guid.NewGuid(); 87 | 88 | foreach (var course in author.Courses) 89 | { 90 | course.Id = Guid.NewGuid(); 91 | } 92 | 93 | _context.Authors.Add(author); 94 | } 95 | 96 | public bool AuthorExists(Guid authorId) 97 | { 98 | if (authorId == Guid.Empty) 99 | { 100 | throw new ArgumentNullException(nameof(authorId)); 101 | } 102 | 103 | return _context.Authors.Any(a => a.Id == authorId); 104 | } 105 | 106 | public void DeleteAuthor(Author author) 107 | { 108 | if (author == null) 109 | { 110 | throw new ArgumentNullException(nameof(author)); 111 | } 112 | 113 | _context.Authors.Remove(author); 114 | } 115 | 116 | public Author GetAuthor(Guid authorId) 117 | { 118 | if (authorId == Guid.Empty) 119 | { 120 | throw new ArgumentNullException(nameof(authorId)); 121 | } 122 | 123 | return _context.Authors.FirstOrDefault(a => a.Id == authorId); 124 | } 125 | 126 | public IEnumerable GetAuthors() 127 | { 128 | return _context.Authors.ToList(); 129 | } 130 | 131 | public PagedList GetAuthors(AuthorsResourceParameters authorsResourceParameters) 132 | { 133 | if (authorsResourceParameters == null) 134 | { 135 | throw new ArgumentNullException(nameof(authorsResourceParameters)); 136 | } 137 | 138 | var collection = _context.Authors as IQueryable; 139 | 140 | if (!string.IsNullOrWhiteSpace(authorsResourceParameters.MainCategory)) 141 | { 142 | var mainCategory = authorsResourceParameters.MainCategory.Trim(); 143 | collection = collection.Where(a => a.MainCategory == mainCategory); 144 | } 145 | 146 | if (!string.IsNullOrWhiteSpace(authorsResourceParameters.SearchQuery)) 147 | { 148 | 149 | var searchQuery = authorsResourceParameters.SearchQuery.Trim(); 150 | collection = collection.Where(a => a.MainCategory.Contains(searchQuery) 151 | || a.FirstName.Contains(searchQuery) 152 | || a.LastName.Contains(searchQuery)); 153 | } 154 | 155 | if (!string.IsNullOrWhiteSpace(authorsResourceParameters.OrderBy)) 156 | { 157 | // get property mapping dictionary 158 | var authorPropertyMappingDictionary = 159 | _propertyMappingService.GetPropertyMapping(); 160 | 161 | collection = collection.ApplySort(authorsResourceParameters.OrderBy, 162 | authorPropertyMappingDictionary); 163 | } 164 | 165 | return PagedList.Create(collection, 166 | authorsResourceParameters.PageNumber, 167 | authorsResourceParameters.PageSize); 168 | } 169 | 170 | public IEnumerable GetAuthors(IEnumerable authorIds) 171 | { 172 | if (authorIds == null) 173 | { 174 | throw new ArgumentNullException(nameof(authorIds)); 175 | } 176 | 177 | return _context.Authors.Where(a => authorIds.Contains(a.Id)) 178 | .OrderBy(a => a.FirstName) 179 | .ThenBy(a => a.LastName) 180 | .ToList(); 181 | } 182 | 183 | public void UpdateAuthor(Author author) 184 | { 185 | // no code in this implementation 186 | } 187 | 188 | public bool Save() 189 | { 190 | return (_context.SaveChanges() >= 0); 191 | } 192 | 193 | public void Dispose() 194 | { 195 | Dispose(true); 196 | GC.SuppressFinalize(this); 197 | } 198 | 199 | protected virtual void Dispose(bool disposing) 200 | { 201 | if (disposing) 202 | { 203 | // dispose resources when needed 204 | } 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Services/ICourseLibraryRepository.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.Entities; 2 | using CourseLibrary.API.Helpers; 3 | using CourseLibrary.API.ResourceParameters; 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace CourseLibrary.API.Services 8 | { 9 | public interface ICourseLibraryRepository 10 | { 11 | IEnumerable GetCourses(Guid authorId); 12 | Course GetCourse(Guid authorId, Guid courseId); 13 | void AddCourse(Guid authorId, Course course); 14 | void UpdateCourse(Course course); 15 | void DeleteCourse(Course course); 16 | IEnumerable GetAuthors(); 17 | PagedList GetAuthors(AuthorsResourceParameters authorsResourceParameters); 18 | Author GetAuthor(Guid authorId); 19 | IEnumerable GetAuthors(IEnumerable authorIds); 20 | void AddAuthor(Author author); 21 | void DeleteAuthor(Author author); 22 | void UpdateAuthor(Author author); 23 | bool AuthorExists(Guid authorId); 24 | bool Save(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Services/IPropertyCheckerService.cs: -------------------------------------------------------------------------------- 1 | namespace CourseLibrary.API.Services 2 | { 3 | public interface IPropertyCheckerService 4 | { 5 | bool TypeHasProperties(string fields); 6 | } 7 | } -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Services/IPropertyMapping.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.Services 7 | { 8 | public interface IPropertyMapping 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Services/IPropertyMappingService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace CourseLibrary.API.Services 4 | { 5 | public interface IPropertyMappingService 6 | { 7 | Dictionary GetPropertyMapping(); 8 | bool ValidMappingExistsFor(string fields); 9 | } 10 | } -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Services/PropertyCheckerService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Threading.Tasks; 6 | 7 | namespace CourseLibrary.API.Services 8 | { 9 | public class PropertyCheckerService : IPropertyCheckerService 10 | { 11 | public bool TypeHasProperties(string fields) 12 | { 13 | if (string.IsNullOrWhiteSpace(fields)) 14 | { 15 | return true; 16 | } 17 | 18 | // the field are separated by ",", so we split it. 19 | var fieldsAfterSplit = fields.Split(','); 20 | 21 | // check if the requested fields exist on source 22 | foreach (var field in fieldsAfterSplit) 23 | { 24 | // trim each field, as it might contain leading 25 | // or trailing spaces. Can't trim the var in foreach, 26 | // so use another var. 27 | var propertyName = field.Trim(); 28 | 29 | // use reflection to check if the property can be 30 | // found on T. 31 | var propertyInfo = typeof(T) 32 | .GetProperty(propertyName, 33 | BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); 34 | 35 | // it can't be found, return false 36 | if (propertyInfo == null) 37 | { 38 | return false; 39 | } 40 | } 41 | 42 | // all checks out, return true 43 | return true; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Services/PropertyMapping.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.Services 7 | { 8 | public class PropertyMapping : IPropertyMapping 9 | { 10 | public Dictionary _mappingDictionary { get; private set; } 11 | 12 | public PropertyMapping(Dictionary mappingDictionary) 13 | { 14 | _mappingDictionary = mappingDictionary ?? 15 | throw new ArgumentNullException(nameof(mappingDictionary)); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Services/PropertyMappingService.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.Entities; 2 | using CourseLibrary.API.Models; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace CourseLibrary.API.Services 9 | { 10 | public class PropertyMappingService : IPropertyMappingService 11 | { 12 | private Dictionary _authorPropertyMapping = 13 | new Dictionary(StringComparer.OrdinalIgnoreCase) 14 | { 15 | { "Id", new PropertyMappingValue(new List() { "Id" } ) }, 16 | { "MainCategory", new PropertyMappingValue(new List() { "MainCategory" } )}, 17 | { "Age", new PropertyMappingValue(new List() { "DateOfBirth" } , true) }, 18 | { "Name", new PropertyMappingValue(new List() { "FirstName", "LastName" }) } 19 | }; 20 | 21 | private IList _propertyMappings = new List(); 22 | 23 | public PropertyMappingService() 24 | { 25 | _propertyMappings.Add(new PropertyMapping(_authorPropertyMapping)); 26 | } 27 | 28 | public bool ValidMappingExistsFor(string fields) 29 | { 30 | var propertyMapping = GetPropertyMapping(); 31 | 32 | if (string.IsNullOrWhiteSpace(fields)) 33 | { 34 | return true; 35 | } 36 | 37 | // the string is separated by ",", so we split it. 38 | var fieldsAfterSplit = fields.Split(','); 39 | 40 | // run through the fields clauses 41 | foreach (var field in fieldsAfterSplit) 42 | { 43 | // trim 44 | var trimmedField = field.Trim(); 45 | 46 | // remove everything after the first " " - if the fields 47 | // are coming from an orderBy string, this part must be 48 | // ignored 49 | var indexOfFirstSpace = trimmedField.IndexOf(" "); 50 | var propertyName = indexOfFirstSpace == -1 ? 51 | trimmedField : trimmedField.Remove(indexOfFirstSpace); 52 | 53 | // find the matching property 54 | if (!propertyMapping.ContainsKey(propertyName)) 55 | { 56 | return false; 57 | } 58 | } 59 | return true; 60 | } 61 | 62 | 63 | public Dictionary GetPropertyMapping 64 | () 65 | { 66 | // get matching mapping 67 | var matchingMapping = _propertyMappings 68 | .OfType>(); 69 | 70 | if (matchingMapping.Count() == 1) 71 | { 72 | return matchingMapping.First()._mappingDictionary; 73 | } 74 | 75 | throw new Exception($"Cannot find exact property mapping instance " + 76 | $"for <{typeof(TSource)},{typeof(TDestination)}"); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Services/PropertyMappingValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.Services 7 | { 8 | public class PropertyMappingValue 9 | { 10 | public IEnumerable DestinationProperties { get; private set; } 11 | public bool Revert { get; private set; } 12 | 13 | public PropertyMappingValue(IEnumerable destinationProperties, 14 | bool revert = false) 15 | { 16 | DestinationProperties = destinationProperties 17 | ?? throw new ArgumentNullException(nameof(destinationProperties)); 18 | Revert = revert; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/Startup.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CourseLibrary.API.DbContexts; 3 | using CourseLibrary.API.Services; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.Formatters; 9 | using Microsoft.AspNetCore.Mvc.Infrastructure; 10 | using Microsoft.EntityFrameworkCore; 11 | using Microsoft.Extensions.Configuration; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Hosting; 14 | using Newtonsoft.Json.Serialization; 15 | using System; 16 | using System.Linq; 17 | 18 | namespace CourseLibrary.API 19 | { 20 | public class Startup 21 | { 22 | public Startup(IConfiguration configuration) 23 | { 24 | Configuration = configuration; 25 | } 26 | 27 | public IConfiguration Configuration { get; } 28 | 29 | // This method gets called by the runtime. Use this method to add services to the container. 30 | public void ConfigureServices(IServiceCollection services) 31 | { 32 | services.AddHttpCacheHeaders((expirationModelOptions) => 33 | { 34 | expirationModelOptions.MaxAge = 60; 35 | expirationModelOptions.CacheLocation = Marvin.Cache.Headers.CacheLocation.Private; 36 | }, 37 | (validationModelOptions) => 38 | { 39 | validationModelOptions.MustRevalidate = true; 40 | }); 41 | 42 | services.AddResponseCaching(); 43 | 44 | services.AddControllers(setupAction => 45 | { 46 | setupAction.ReturnHttpNotAcceptable = true; 47 | setupAction.CacheProfiles.Add("240SecondsCacheProfile", 48 | new CacheProfile() 49 | { 50 | Duration = 240 51 | }); 52 | }).AddNewtonsoftJson(setupAction => 53 | { 54 | setupAction.SerializerSettings.ContractResolver = 55 | new CamelCasePropertyNamesContractResolver(); 56 | }) 57 | .AddXmlDataContractSerializerFormatters() 58 | .ConfigureApiBehaviorOptions(setupAction => 59 | { 60 | setupAction.InvalidModelStateResponseFactory = context => 61 | { 62 | // create a problem details object 63 | var problemDetailsFactory = context.HttpContext.RequestServices 64 | .GetRequiredService(); 65 | var problemDetails = problemDetailsFactory.CreateValidationProblemDetails( 66 | context.HttpContext, 67 | context.ModelState); 68 | 69 | // add additional info not added by default 70 | problemDetails.Detail = "See the errors field for details."; 71 | problemDetails.Instance = context.HttpContext.Request.Path; 72 | 73 | // find out which status code to use 74 | var actionExecutingContext = 75 | context as Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext; 76 | 77 | // if there are modelstate errors & all keys were correctly 78 | // found/parsed we're dealing with validation errors 79 | // 80 | // if the context couldn't be cast to an ActionExecutingContext 81 | // because it's a ControllerContext, we're dealing with an issue 82 | // that happened after the initial input was correctly parsed. 83 | // This happens, for example, when manually validating an object inside 84 | // of a controller action. That means that by then all keys 85 | // WERE correctly found and parsed. In that case, we're 86 | // thus also dealing with a validation error. 87 | if (context.ModelState.ErrorCount > 0 && 88 | (context is ControllerContext || 89 | actionExecutingContext?.ActionArguments.Count == context.ActionDescriptor.Parameters.Count)) 90 | { 91 | problemDetails.Type = "https://courselibrary.com/modelvalidationproblem"; 92 | problemDetails.Status = StatusCodes.Status422UnprocessableEntity; 93 | problemDetails.Title = "One or more validation errors occurred."; 94 | 95 | return new UnprocessableEntityObjectResult(problemDetails) 96 | { 97 | ContentTypes = { "application/problem+json" } 98 | }; 99 | } 100 | 101 | // if one of the arguments wasn't correctly found / couldn't be parsed 102 | // we're dealing with null/unparseable input 103 | problemDetails.Status = StatusCodes.Status400BadRequest; 104 | problemDetails.Title = "One or more errors on input occurred."; 105 | return new BadRequestObjectResult(problemDetails) 106 | { 107 | ContentTypes = { "application/problem+json" } 108 | }; 109 | }; 110 | }); 111 | 112 | services.Configure(config => 113 | { 114 | var newtonsoftJsonOutputFormatter = config.OutputFormatters 115 | .OfType()?.FirstOrDefault(); 116 | 117 | if (newtonsoftJsonOutputFormatter != null) 118 | { 119 | newtonsoftJsonOutputFormatter.SupportedMediaTypes.Add("application/vnd.marvin.hateoas+json"); 120 | } 121 | }); 122 | 123 | // register PropertyMappingService 124 | services.AddTransient(); 125 | 126 | // register PropertyCheckerService 127 | services.AddTransient(); 128 | 129 | services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); 130 | 131 | services.AddScoped(); 132 | 133 | services.AddDbContext(options => 134 | { 135 | options.UseSqlServer( 136 | @"Server=(localdb)\mssqllocaldb;Database=CourseLibraryDB;Trusted_Connection=True;"); 137 | }); 138 | } 139 | 140 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 141 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 142 | { 143 | if (env.IsDevelopment()) 144 | { 145 | app.UseDeveloperExceptionPage(); 146 | } 147 | else 148 | { 149 | app.UseExceptionHandler(appBuilder => 150 | { 151 | appBuilder.Run(async context => 152 | { 153 | context.Response.StatusCode = 500; 154 | await context.Response.WriteAsync("An unexpected fault happened. Try again later."); 155 | }); 156 | }); 157 | 158 | } 159 | 160 | // app.UseResponseCaching(); 161 | 162 | app.UseHttpCacheHeaders(); 163 | 164 | app.UseRouting(); 165 | 166 | app.UseAuthorization(); 167 | 168 | app.UseEndpoints(endpoints => 169 | { 170 | endpoints.MapControllers(); 171 | }); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/ValidationAttributes/CourseTitleMustBeDifferentFromDescriptionAttribute.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace CourseLibrary.API.ValidationAttributes 9 | { 10 | public class CourseTitleMustBeDifferentFromDescriptionAttribute : ValidationAttribute 11 | { 12 | protected override ValidationResult IsValid(object value, 13 | ValidationContext validationContext) 14 | { 15 | var course = (CourseForManipulationDto)validationContext.ObjectInstance; 16 | 17 | if (course.Title == course.Description) 18 | { 19 | return new ValidationResult(ErrorMessage, 20 | new[] { nameof(CourseForManipulationDto) }); 21 | } 22 | 23 | return ValidationResult.Success; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /Finished sample/CourseLibrary/CourseLibrary.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29009.5 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CourseLibrary.API", "CourseLibrary.API\CourseLibrary.API.csproj", "{2900C008-F2EC-4A0D-8074-F47EB9F84036}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {2900C008-F2EC-4A0D-8074-F47EB9F84036}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {2900C008-F2EC-4A0D-8074-F47EB9F84036}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {2900C008-F2EC-4A0D-8074-F47EB9F84036}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {2900C008-F2EC-4A0D-8074-F47EB9F84036}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {08E0F198-B49E-42B9-9DB8-20DD2062C0CA} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kevin Dockx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Implementing Advanced RESTful Concerns with ASP.NET Core 3 2 | Fully functioning sample application accompanying my Implementing Advanced RESTful Concerns with ASP.NET Core 3 course. 3 | 4 | The course tackles paging, sorting, data shaping, HATEOAS, advanced content negotation, custom media types, caching (in-depth) and concurrency. If you're interested, you can find it here: https://app.pluralsight.com/library/courses/asp-dot-net-core-3-advanced-restful-concerns/table-of-contents 5 | 6 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Controllers/AuthorCollectionsController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CourseLibrary.API.Helpers; 3 | using CourseLibrary.API.Models; 4 | using CourseLibrary.API.Services; 5 | using Microsoft.AspNetCore.Mvc; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | 11 | namespace CourseLibrary.API.Controllers 12 | { 13 | [ApiController] 14 | [Route("api/authorcollections")] 15 | public class AuthorCollectionsController : ControllerBase 16 | { 17 | private readonly ICourseLibraryRepository _courseLibraryRepository; 18 | private readonly IMapper _mapper; 19 | 20 | public AuthorCollectionsController(ICourseLibraryRepository courseLibraryRepository, 21 | IMapper mapper) 22 | { 23 | _courseLibraryRepository = courseLibraryRepository ?? 24 | throw new ArgumentNullException(nameof(courseLibraryRepository)); 25 | _mapper = mapper ?? 26 | throw new ArgumentNullException(nameof(mapper)); 27 | } 28 | 29 | [HttpGet("({ids})", Name ="GetAuthorCollection")] 30 | public IActionResult GetAuthorCollection( 31 | [FromRoute] 32 | [ModelBinder(BinderType = typeof(ArrayModelBinder))] IEnumerable ids) 33 | { 34 | if (ids == null) 35 | { 36 | return BadRequest(); 37 | } 38 | 39 | var authorEntities = _courseLibraryRepository.GetAuthors(ids); 40 | 41 | if (ids.Count() != authorEntities.Count()) 42 | { 43 | return NotFound(); 44 | } 45 | 46 | var authorsToReturn = _mapper.Map>(authorEntities); 47 | 48 | return Ok(authorsToReturn); 49 | } 50 | 51 | 52 | [HttpPost] 53 | public ActionResult> CreateAuthorCollection( 54 | IEnumerable authorCollection) 55 | { 56 | var authorEntities = _mapper.Map>(authorCollection); 57 | foreach (var author in authorEntities) 58 | { 59 | _courseLibraryRepository.AddAuthor(author); 60 | } 61 | 62 | _courseLibraryRepository.Save(); 63 | 64 | var authorCollectionToReturn = _mapper.Map>(authorEntities); 65 | var idsAsString = string.Join(",", authorCollectionToReturn.Select(a => a.Id)); 66 | return CreatedAtRoute("GetAuthorCollection", 67 | new { ids = idsAsString }, 68 | authorCollectionToReturn); 69 | } 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Controllers/AuthorsController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CourseLibrary.API.Helpers; 3 | using CourseLibrary.API.Models; 4 | using CourseLibrary.API.ResourceParameters; 5 | using CourseLibrary.API.Services; 6 | using Microsoft.AspNetCore.Mvc; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | 12 | namespace CourseLibrary.API.Controllers 13 | { 14 | [ApiController] 15 | [Route("api/authors")] 16 | public class AuthorsController : ControllerBase 17 | { 18 | private readonly ICourseLibraryRepository _courseLibraryRepository; 19 | private readonly IMapper _mapper; 20 | 21 | public AuthorsController(ICourseLibraryRepository courseLibraryRepository, 22 | IMapper mapper) 23 | { 24 | _courseLibraryRepository = courseLibraryRepository ?? 25 | throw new ArgumentNullException(nameof(courseLibraryRepository)); 26 | _mapper = mapper ?? 27 | throw new ArgumentNullException(nameof(mapper)); 28 | } 29 | 30 | [HttpGet()] 31 | [HttpHead] 32 | public ActionResult> GetAuthors( 33 | [FromQuery] AuthorsResourceParameters authorsResourceParameters) 34 | { 35 | var authorsFromRepo = _courseLibraryRepository.GetAuthors(authorsResourceParameters); 36 | return Ok(_mapper.Map>(authorsFromRepo)); 37 | } 38 | 39 | [HttpGet("{authorId}", Name ="GetAuthor")] 40 | public IActionResult GetAuthor(Guid authorId) 41 | { 42 | var authorFromRepo = _courseLibraryRepository.GetAuthor(authorId); 43 | 44 | if (authorFromRepo == null) 45 | { 46 | return NotFound(); 47 | } 48 | 49 | return Ok(_mapper.Map(authorFromRepo)); 50 | } 51 | 52 | [HttpPost] 53 | public ActionResult CreateAuthor(AuthorForCreationDto author) 54 | { 55 | var authorEntity = _mapper.Map(author); 56 | _courseLibraryRepository.AddAuthor(authorEntity); 57 | _courseLibraryRepository.Save(); 58 | 59 | var authorToReturn = _mapper.Map(authorEntity); 60 | return CreatedAtRoute("GetAuthor", 61 | new { authorId = authorToReturn.Id }, 62 | authorToReturn); 63 | } 64 | 65 | [HttpOptions] 66 | public IActionResult GetAuthorsOptions() 67 | { 68 | Response.Headers.Add("Allow", "GET,OPTIONS,POST"); 69 | return Ok(); 70 | } 71 | 72 | [HttpDelete("{authorId}")] 73 | public ActionResult DeleteAuthor(Guid authorId) 74 | { 75 | var authorFromRepo = _courseLibraryRepository.GetAuthor(authorId); 76 | 77 | if (authorFromRepo == null) 78 | { 79 | return NotFound(); 80 | } 81 | 82 | _courseLibraryRepository.DeleteAuthor(authorFromRepo); 83 | 84 | _courseLibraryRepository.Save(); 85 | 86 | return NoContent(); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Controllers/CoursesController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CourseLibrary.API.Models; 3 | using CourseLibrary.API.Services; 4 | using Microsoft.AspNetCore.JsonPatch; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.Infrastructure; 7 | using Microsoft.AspNetCore.Mvc.ModelBinding; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Options; 10 | using System; 11 | using System.Collections.Generic; 12 | using System.Linq; 13 | using System.Threading.Tasks; 14 | 15 | namespace CourseLibrary.API.Controllers 16 | { 17 | [ApiController] 18 | [Route("api/authors/{authorId}/courses")] 19 | public class CoursesController : ControllerBase 20 | { 21 | private readonly ICourseLibraryRepository _courseLibraryRepository; 22 | private readonly IMapper _mapper; 23 | 24 | public CoursesController(ICourseLibraryRepository courseLibraryRepository, 25 | IMapper mapper) 26 | { 27 | _courseLibraryRepository = courseLibraryRepository ?? 28 | throw new ArgumentNullException(nameof(courseLibraryRepository)); 29 | _mapper = mapper ?? 30 | throw new ArgumentNullException(nameof(mapper)); 31 | } 32 | 33 | [HttpGet] 34 | public ActionResult> GetCoursesForAuthor(Guid authorId) 35 | { 36 | if (!_courseLibraryRepository.AuthorExists(authorId)) 37 | { 38 | return NotFound(); 39 | } 40 | 41 | var coursesForAuthorFromRepo = _courseLibraryRepository.GetCourses(authorId); 42 | return Ok(_mapper.Map>(coursesForAuthorFromRepo)); 43 | } 44 | 45 | [HttpGet("{courseId}", Name = "GetCourseForAuthor")] 46 | public ActionResult GetCourseForAuthor(Guid authorId, Guid courseId) 47 | { 48 | if (!_courseLibraryRepository.AuthorExists(authorId)) 49 | { 50 | return NotFound(); 51 | } 52 | 53 | var courseForAuthorFromRepo = _courseLibraryRepository.GetCourse(authorId, courseId); 54 | 55 | if (courseForAuthorFromRepo == null) 56 | { 57 | return NotFound(); 58 | } 59 | 60 | return Ok(_mapper.Map(courseForAuthorFromRepo)); 61 | } 62 | 63 | [HttpPost] 64 | public ActionResult CreateCourseForAuthor( 65 | Guid authorId, CourseForCreationDto course) 66 | { 67 | if (!_courseLibraryRepository.AuthorExists(authorId)) 68 | { 69 | return NotFound(); 70 | } 71 | 72 | var courseEntity = _mapper.Map(course); 73 | _courseLibraryRepository.AddCourse(authorId, courseEntity); 74 | _courseLibraryRepository.Save(); 75 | 76 | var courseToReturn = _mapper.Map(courseEntity); 77 | return CreatedAtRoute("GetCourseForAuthor", 78 | new { authorId = authorId, courseId = courseToReturn.Id }, 79 | courseToReturn); 80 | } 81 | 82 | [HttpPut("{courseId}")] 83 | public IActionResult UpdateCourseForAuthor(Guid authorId, 84 | Guid courseId, 85 | CourseForUpdateDto course) 86 | { 87 | if (!_courseLibraryRepository.AuthorExists(authorId)) 88 | { 89 | return NotFound(); 90 | } 91 | 92 | var courseForAuthorFromRepo = _courseLibraryRepository.GetCourse(authorId, courseId); 93 | 94 | if (courseForAuthorFromRepo == null) 95 | { 96 | var courseToAdd = _mapper.Map(course); 97 | courseToAdd.Id = courseId; 98 | 99 | _courseLibraryRepository.AddCourse(authorId, courseToAdd); 100 | 101 | _courseLibraryRepository.Save(); 102 | 103 | var courseToReturn = _mapper.Map(courseToAdd); 104 | 105 | return CreatedAtRoute("GetCourseForAuthor", 106 | new { authorId, courseId = courseToReturn.Id }, 107 | courseToReturn); 108 | } 109 | 110 | // map the entity to a CourseForUpdateDto 111 | // apply the updated field values to that dto 112 | // map the CourseForUpdateDto back to an entity 113 | _mapper.Map(course, courseForAuthorFromRepo); 114 | 115 | _courseLibraryRepository.UpdateCourse(courseForAuthorFromRepo); 116 | 117 | _courseLibraryRepository.Save(); 118 | return NoContent(); 119 | } 120 | 121 | [HttpPatch("{courseId}")] 122 | public ActionResult PartiallyUpdateCourseForAuthor(Guid authorId, 123 | Guid courseId, 124 | JsonPatchDocument patchDocument) 125 | { 126 | if (!_courseLibraryRepository.AuthorExists(authorId)) 127 | { 128 | return NotFound(); 129 | } 130 | 131 | var courseForAuthorFromRepo = _courseLibraryRepository.GetCourse(authorId, courseId); 132 | 133 | if (courseForAuthorFromRepo == null) 134 | { 135 | var courseDto = new CourseForUpdateDto(); 136 | patchDocument.ApplyTo(courseDto, ModelState); 137 | 138 | if (!TryValidateModel(courseDto)) 139 | { 140 | return ValidationProblem(ModelState); 141 | } 142 | 143 | var courseToAdd = _mapper.Map(courseDto); 144 | courseToAdd.Id = courseId; 145 | 146 | _courseLibraryRepository.AddCourse(authorId, courseToAdd); 147 | _courseLibraryRepository.Save(); 148 | 149 | var courseToReturn = _mapper.Map(courseToAdd); 150 | 151 | return CreatedAtRoute("GetCourseForAuthor", 152 | new { authorId, courseId = courseToReturn.Id }, 153 | courseToReturn); 154 | } 155 | 156 | var courseToPatch = _mapper.Map(courseForAuthorFromRepo); 157 | // add validation 158 | patchDocument.ApplyTo(courseToPatch, ModelState); 159 | 160 | if (!TryValidateModel(courseToPatch)) 161 | { 162 | return ValidationProblem(ModelState); 163 | } 164 | 165 | _mapper.Map(courseToPatch, courseForAuthorFromRepo); 166 | 167 | _courseLibraryRepository.UpdateCourse(courseForAuthorFromRepo); 168 | 169 | _courseLibraryRepository.Save(); 170 | 171 | return NoContent(); 172 | } 173 | 174 | [HttpDelete("{courseId}")] 175 | public ActionResult DeleteCourseForAuthor(Guid authorId, Guid courseId) 176 | { 177 | if (!_courseLibraryRepository.AuthorExists(authorId)) 178 | { 179 | return NotFound(); 180 | } 181 | 182 | var courseForAuthorFromRepo = _courseLibraryRepository.GetCourse(authorId, courseId); 183 | 184 | if (courseForAuthorFromRepo == null) 185 | { 186 | return NotFound(); 187 | } 188 | 189 | _courseLibraryRepository.DeleteCourse(courseForAuthorFromRepo); 190 | _courseLibraryRepository.Save(); 191 | 192 | return NoContent(); 193 | } 194 | 195 | public override ActionResult ValidationProblem( 196 | [ActionResultObjectValue] ModelStateDictionary modelStateDictionary) 197 | { 198 | var options = HttpContext.RequestServices 199 | .GetRequiredService>(); 200 | return (ActionResult)options.Value.InvalidModelStateResponseFactory(ControllerContext); 201 | } 202 | } 203 | } -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/CourseLibrary.API.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/DbContexts/CourseLibraryContext.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using System; 4 | 5 | namespace CourseLibrary.API.DbContexts 6 | { 7 | public class CourseLibraryContext : DbContext 8 | { 9 | public CourseLibraryContext(DbContextOptions options) 10 | : base(options) 11 | { 12 | } 13 | 14 | public DbSet Authors { get; set; } 15 | public DbSet Courses { get; set; } 16 | 17 | protected override void OnModelCreating(ModelBuilder modelBuilder) 18 | { 19 | // seed the database with dummy data 20 | modelBuilder.Entity().HasData( 21 | new Author() 22 | { 23 | Id = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 24 | FirstName = "Berry", 25 | LastName = "Griffin Beak Eldritch", 26 | DateOfBirth = new DateTime(1650, 7, 23), 27 | MainCategory = "Ships" 28 | }, 29 | new Author() 30 | { 31 | Id = Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 32 | FirstName = "Nancy", 33 | LastName = "Swashbuckler Rye", 34 | DateOfBirth = new DateTime(1668, 5, 21), 35 | MainCategory = "Rum" 36 | }, 37 | new Author() 38 | { 39 | Id = Guid.Parse("2902b665-1190-4c70-9915-b9c2d7680450"), 40 | FirstName = "Eli", 41 | LastName = "Ivory Bones Sweet", 42 | DateOfBirth = new DateTime(1701, 12, 16), 43 | MainCategory = "Singing" 44 | }, 45 | new Author() 46 | { 47 | Id = Guid.Parse("102b566b-ba1f-404c-b2df-e2cde39ade09"), 48 | FirstName = "Arnold", 49 | LastName = "The Unseen Stafford", 50 | DateOfBirth = new DateTime(1702, 3, 6), 51 | MainCategory = "Singing" 52 | }, 53 | new Author() 54 | { 55 | Id = Guid.Parse("5b3621c0-7b12-4e80-9c8b-3398cba7ee05"), 56 | FirstName = "Seabury", 57 | LastName = "Toxic Reyson", 58 | DateOfBirth = new DateTime(1690, 11, 23), 59 | MainCategory = "Maps" 60 | }, 61 | new Author() 62 | { 63 | Id = Guid.Parse("2aadd2df-7caf-45ab-9355-7f6332985a87"), 64 | FirstName = "Rutherford", 65 | LastName = "Fearless Cloven", 66 | DateOfBirth = new DateTime(1723, 4, 5), 67 | MainCategory = "General debauchery" 68 | }, 69 | new Author() 70 | { 71 | Id = Guid.Parse("2ee49fe3-edf2-4f91-8409-3eb25ce6ca51"), 72 | FirstName = "Atherton", 73 | LastName = "Crow Ridley", 74 | DateOfBirth = new DateTime(1721, 10, 11), 75 | MainCategory = "Rum" 76 | } 77 | ); 78 | 79 | modelBuilder.Entity().HasData( 80 | new Course 81 | { 82 | Id = Guid.Parse("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 83 | AuthorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 84 | Title = "Commandeering a Ship Without Getting Caught", 85 | 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." 86 | }, 87 | new Course 88 | { 89 | Id = Guid.Parse("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 90 | AuthorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 91 | Title = "Overthrowing Mutiny", 92 | Description = "In this course, the author provides tips to avoid, or, if needed, overthrow pirate mutiny." 93 | }, 94 | new Course 95 | { 96 | Id = Guid.Parse("d173e20d-159e-4127-9ce9-b0ac2564ad97"), 97 | AuthorId = Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 98 | Title = "Avoiding Brawls While Drinking as Much Rum as You Desire", 99 | 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." 100 | }, 101 | new Course 102 | { 103 | Id = Guid.Parse("40ff5488-fdab-45b5-bc3a-14302d59869a"), 104 | AuthorId = Guid.Parse("2902b665-1190-4c70-9915-b9c2d7680450"), 105 | Title = "Singalong Pirate Hits", 106 | 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." 107 | } 108 | ); 109 | 110 | base.OnModelCreating(modelBuilder); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Entities/Author.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace CourseLibrary.API.Entities 6 | { 7 | public class Author 8 | { 9 | [Key] 10 | public Guid Id { get; set; } 11 | 12 | [Required] 13 | [MaxLength(50)] 14 | public string FirstName { get; set; } 15 | 16 | [Required] 17 | [MaxLength(50)] 18 | public string LastName { get; set; } 19 | 20 | [Required] 21 | public DateTimeOffset DateOfBirth { get; set; } 22 | 23 | [Required] 24 | [MaxLength(50)] 25 | public string MainCategory { get; set; } 26 | 27 | public ICollection Courses { get; set; } 28 | = new List(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Entities/Course.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | namespace CourseLibrary.API.Entities 6 | { 7 | public class Course 8 | { 9 | [Key] 10 | public Guid Id { get; set; } 11 | 12 | [Required] 13 | [MaxLength(100)] 14 | public string Title { get; set; } 15 | 16 | [MaxLength(1500)] 17 | public string Description { get; set; } 18 | 19 | [ForeignKey("AuthorId")] 20 | public Author Author { get; set; } 21 | 22 | public Guid AuthorId { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Helpers/ArrayModelBinder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.ModelBinding; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Threading.Tasks; 8 | 9 | namespace CourseLibrary.API.Helpers 10 | { 11 | public class ArrayModelBinder : IModelBinder 12 | { 13 | public Task BindModelAsync(ModelBindingContext bindingContext) 14 | { 15 | // Our binder works only on enumerable types 16 | if (!bindingContext.ModelMetadata.IsEnumerableType) 17 | { 18 | bindingContext.Result = ModelBindingResult.Failed(); 19 | return Task.CompletedTask; 20 | } 21 | 22 | // Get the inputted value through the value provider 23 | var value = bindingContext.ValueProvider 24 | .GetValue(bindingContext.ModelName).ToString(); 25 | 26 | // If that value is null or whitespace, we return null 27 | if (string.IsNullOrWhiteSpace(value)) 28 | { 29 | bindingContext.Result = ModelBindingResult.Success(null); 30 | return Task.CompletedTask; 31 | } 32 | 33 | // The value isn't null or whitespace, 34 | // and the type of the model is enumerable. 35 | // Get the enumerable's type, and a converter 36 | var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0]; 37 | var converter = TypeDescriptor.GetConverter(elementType); 38 | 39 | // Convert each item in the value list to the enumerable type 40 | var values = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) 41 | .Select(x => converter.ConvertFromString(x.Trim())) 42 | .ToArray(); 43 | 44 | // Create an array of that type, and set it as the Model value 45 | var typedValues = Array.CreateInstance(elementType, values.Length); 46 | values.CopyTo(typedValues, 0); 47 | bindingContext.Model = typedValues; 48 | 49 | // return a successful result, passing in the Model 50 | bindingContext.Result = ModelBindingResult.Success(bindingContext.Model); 51 | return Task.CompletedTask; 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Helpers/DateTimeOffsetExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.Helpers 7 | { 8 | public static class DateTimeOffsetExtensions 9 | { 10 | public static int GetCurrentAge(this DateTimeOffset dateTimeOffset) 11 | { 12 | var currentDate = DateTime.UtcNow; 13 | int age = currentDate.Year - dateTimeOffset.Year; 14 | 15 | if (currentDate < dateTimeOffset.AddYears(age)) 16 | { 17 | age--; 18 | } 19 | 20 | return age; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Migrations/CourseLibraryContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using CourseLibrary.API.DbContexts; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace CourseLibrary.API.Migrations 10 | { 11 | [DbContext(typeof(CourseLibraryContext))] 12 | partial class CourseLibraryContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "3.0.0-preview6.19304.10") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("CourseLibrary.API.Entities.Author", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd(); 26 | 27 | b.Property("DateOfBirth"); 28 | 29 | b.Property("DateOfDeath"); 30 | 31 | b.Property("FirstName") 32 | .IsRequired() 33 | .HasMaxLength(50); 34 | 35 | b.Property("LastName") 36 | .IsRequired() 37 | .HasMaxLength(50); 38 | 39 | b.Property("MainCategory") 40 | .IsRequired() 41 | .HasMaxLength(50); 42 | 43 | b.HasKey("Id"); 44 | 45 | b.ToTable("Authors"); 46 | 47 | b.HasData( 48 | new 49 | { 50 | Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 51 | DateOfBirth = new DateTimeOffset(new DateTime(1650, 7, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 52 | FirstName = "Berry", 53 | LastName = "Griffin Beak Eldritch", 54 | MainCategory = "Ships" 55 | }, 56 | new 57 | { 58 | Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 59 | DateOfBirth = new DateTimeOffset(new DateTime(1668, 5, 21, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 60 | FirstName = "Nancy", 61 | LastName = "Swashbuckler Rye", 62 | MainCategory = "Rum" 63 | }, 64 | new 65 | { 66 | Id = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 67 | DateOfBirth = new DateTimeOffset(new DateTime(1701, 12, 16, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 68 | FirstName = "Eli", 69 | LastName = "Ivory Bones Sweet", 70 | MainCategory = "Singing" 71 | }, 72 | new 73 | { 74 | Id = new Guid("102b566b-ba1f-404c-b2df-e2cde39ade09"), 75 | DateOfBirth = new DateTimeOffset(new DateTime(1702, 3, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 76 | FirstName = "Arnold", 77 | LastName = "The Unseen Stafford", 78 | MainCategory = "Singing" 79 | }, 80 | new 81 | { 82 | Id = new Guid("5b3621c0-7b12-4e80-9c8b-3398cba7ee05"), 83 | DateOfBirth = new DateTimeOffset(new DateTime(1690, 11, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 84 | FirstName = "Seabury", 85 | LastName = "Toxic Reyson", 86 | MainCategory = "Maps" 87 | }, 88 | new 89 | { 90 | Id = new Guid("2aadd2df-7caf-45ab-9355-7f6332985a87"), 91 | DateOfBirth = new DateTimeOffset(new DateTime(1723, 4, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 92 | FirstName = "Rutherford", 93 | LastName = "Fearless Cloven", 94 | MainCategory = "General debauchery" 95 | }, 96 | new 97 | { 98 | Id = new Guid("2ee49fe3-edf2-4f91-8409-3eb25ce6ca51"), 99 | DateOfBirth = new DateTimeOffset(new DateTime(1721, 10, 11, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 100 | FirstName = "Atherton", 101 | LastName = "Crow Ridley", 102 | MainCategory = "Rum" 103 | }); 104 | }); 105 | 106 | modelBuilder.Entity("CourseLibrary.API.Entities.Course", b => 107 | { 108 | b.Property("Id") 109 | .ValueGeneratedOnAdd(); 110 | 111 | b.Property("AuthorId"); 112 | 113 | b.Property("Description") 114 | .HasMaxLength(1500); 115 | 116 | b.Property("Title") 117 | .IsRequired() 118 | .HasMaxLength(100); 119 | 120 | b.HasKey("Id"); 121 | 122 | b.HasIndex("AuthorId"); 123 | 124 | b.ToTable("Courses"); 125 | 126 | b.HasData( 127 | new 128 | { 129 | Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 130 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 131 | 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.", 132 | Title = "Commandeering a Ship Without Getting Caught" 133 | }, 134 | new 135 | { 136 | Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 137 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 138 | Description = "In this course, the author provides tips to avoid, or, if needed, overthrow pirate mutiny.", 139 | Title = "Overthrowing Mutiny" 140 | }, 141 | new 142 | { 143 | Id = new Guid("d173e20d-159e-4127-9ce9-b0ac2564ad97"), 144 | AuthorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 145 | 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.", 146 | Title = "Avoiding Brawls While Drinking as Much Rum as You Desire" 147 | }, 148 | new 149 | { 150 | Id = new Guid("40ff5488-fdab-45b5-bc3a-14302d59869a"), 151 | AuthorId = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 152 | 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.", 153 | Title = "Singalong Pirate Hits" 154 | }); 155 | }); 156 | 157 | modelBuilder.Entity("CourseLibrary.API.Entities.Course", b => 158 | { 159 | b.HasOne("CourseLibrary.API.Entities.Author", "Author") 160 | .WithMany("Courses") 161 | .HasForeignKey("AuthorId") 162 | .OnDelete(DeleteBehavior.Cascade) 163 | .IsRequired(); 164 | }); 165 | #pragma warning restore 612, 618 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Migrations/InitialMigration.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using CourseLibrary.API.DbContexts; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 9 | 10 | namespace CourseLibrary.API.Migrations 11 | { 12 | [DbContext(typeof(CourseLibraryContext))] 13 | [Migration("20190731122611_InitialMigration")] 14 | partial class InitialMigration 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "3.0.0-preview7.19362.6") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("CourseLibrary.API.Entities.Author", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd(); 28 | 29 | b.Property("DateOfBirth"); 30 | 31 | b.Property("FirstName") 32 | .IsRequired() 33 | .HasMaxLength(50); 34 | 35 | b.Property("LastName") 36 | .IsRequired() 37 | .HasMaxLength(50); 38 | 39 | b.Property("MainCategory") 40 | .IsRequired() 41 | .HasMaxLength(50); 42 | 43 | b.HasKey("Id"); 44 | 45 | b.ToTable("Authors"); 46 | 47 | b.HasData( 48 | new 49 | { 50 | Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 51 | DateOfBirth = new DateTimeOffset(new DateTime(1970, 7, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 52 | FirstName = "Berry", 53 | LastName = "Griffin Beak Eldritch", 54 | MainCategory = "Ships" 55 | }, 56 | new 57 | { 58 | Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 59 | DateOfBirth = new DateTimeOffset(new DateTime(1968, 5, 21, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 60 | FirstName = "Nancy", 61 | LastName = "Swashbuckler Rye", 62 | MainCategory = "Rum" 63 | }, 64 | new 65 | { 66 | Id = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 67 | DateOfBirth = new DateTimeOffset(new DateTime(1991, 12, 16, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 68 | FirstName = "Eli", 69 | LastName = "Ivory Bones Sweet", 70 | MainCategory = "Singing" 71 | }, 72 | new 73 | { 74 | Id = new Guid("102b566b-ba1f-404c-b2df-e2cde39ade09"), 75 | DateOfBirth = new DateTimeOffset(new DateTime(1984, 3, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 76 | FirstName = "Arnold", 77 | LastName = "The Unseen Stafford", 78 | MainCategory = "Singing" 79 | }, 80 | new 81 | { 82 | Id = new Guid("5b3621c0-7b12-4e80-9c8b-3398cba7ee05"), 83 | DateOfBirth = new DateTimeOffset(new DateTime(1990, 11, 23, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 84 | FirstName = "Seabury", 85 | LastName = "Toxic Reyson", 86 | MainCategory = "Maps" 87 | }, 88 | new 89 | { 90 | Id = new Guid("2aadd2df-7caf-45ab-9355-7f6332985a87"), 91 | DateOfBirth = new DateTimeOffset(new DateTime(1978, 4, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 92 | FirstName = "Rutherford", 93 | LastName = "Fearless Cloven", 94 | MainCategory = "General debauchery" 95 | }, 96 | new 97 | { 98 | Id = new Guid("2ee49fe3-edf2-4f91-8409-3eb25ce6ca51"), 99 | DateOfBirth = new DateTimeOffset(new DateTime(1959, 10, 11, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 100 | FirstName = "Atherton", 101 | LastName = "Crow Ridley", 102 | MainCategory = "Rum" 103 | }, 104 | new 105 | { 106 | Id = new Guid("71838f8b-6ab3-4539-9e67-4e77b8ede1c0"), 107 | DateOfBirth = new DateTimeOffset(new DateTime(1969, 8, 11, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 108 | FirstName = "Huxford", 109 | LastName = "The Hawk Morris", 110 | MainCategory = "Maps" 111 | }, 112 | new 113 | { 114 | Id = new Guid("119f9ccb-149d-4d3c-ad4f-40100f38e918"), 115 | DateOfBirth = new DateTimeOffset(new DateTime(1972, 1, 8, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 116 | FirstName = "Dwennon", 117 | LastName = "Rigger Quye", 118 | MainCategory = "Maps" 119 | }, 120 | new 121 | { 122 | Id = new Guid("28c1db41-f104-46e6-8943-d31c0291e0e3"), 123 | DateOfBirth = new DateTimeOffset(new DateTime(1982, 5, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 124 | FirstName = "Rushford", 125 | LastName = "Subtle Asema", 126 | MainCategory = "Rum" 127 | }, 128 | new 129 | { 130 | Id = new Guid("d94a64c2-2e8f-4162-9976-0ffe03d30767"), 131 | DateOfBirth = new DateTimeOffset(new DateTime(1976, 7, 12, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 132 | FirstName = "Hagley", 133 | LastName = "Imposter Grendel", 134 | MainCategory = "Singing" 135 | }, 136 | new 137 | { 138 | Id = new Guid("380c2c6b-0d1c-4b82-9d83-3cf635a3e62b"), 139 | DateOfBirth = new DateTimeOffset(new DateTime(1977, 2, 8, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 140 | FirstName = "Mabel", 141 | LastName = "Barnacle Grendel", 142 | MainCategory = "Maps" 143 | }); 144 | }); 145 | 146 | modelBuilder.Entity("CourseLibrary.API.Entities.Course", b => 147 | { 148 | b.Property("Id") 149 | .ValueGeneratedOnAdd(); 150 | 151 | b.Property("AuthorId"); 152 | 153 | b.Property("Description") 154 | .HasMaxLength(1500); 155 | 156 | b.Property("Title") 157 | .IsRequired() 158 | .HasMaxLength(100); 159 | 160 | b.HasKey("Id"); 161 | 162 | b.HasIndex("AuthorId"); 163 | 164 | b.ToTable("Courses"); 165 | 166 | b.HasData( 167 | new 168 | { 169 | Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 170 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 171 | 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.", 172 | Title = "Commandeering a Ship Without Getting Caught" 173 | }, 174 | new 175 | { 176 | Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 177 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 178 | Description = "In this course, the author provides tips to avoid, or, if needed, overthrow pirate mutiny.", 179 | Title = "Overthrowing Mutiny" 180 | }, 181 | new 182 | { 183 | Id = new Guid("d173e20d-159e-4127-9ce9-b0ac2564ad97"), 184 | AuthorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 185 | 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.", 186 | Title = "Avoiding Brawls While Drinking as Much Rum as You Desire" 187 | }, 188 | new 189 | { 190 | Id = new Guid("40ff5488-fdab-45b5-bc3a-14302d59869a"), 191 | AuthorId = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 192 | 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.", 193 | Title = "Singalong Pirate Hits" 194 | }); 195 | }); 196 | 197 | modelBuilder.Entity("CourseLibrary.API.Entities.Course", b => 198 | { 199 | b.HasOne("CourseLibrary.API.Entities.Author", "Author") 200 | .WithMany("Courses") 201 | .HasForeignKey("AuthorId") 202 | .OnDelete(DeleteBehavior.Cascade) 203 | .IsRequired(); 204 | }); 205 | #pragma warning restore 612, 618 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Migrations/InitialMigration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace CourseLibrary.API.Migrations 5 | { 6 | public partial class InitialMigration : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "Authors", 12 | columns: table => new 13 | { 14 | Id = table.Column(nullable: false), 15 | FirstName = table.Column(maxLength: 50, nullable: false), 16 | LastName = table.Column(maxLength: 50, nullable: false), 17 | DateOfBirth = table.Column(nullable: false), 18 | MainCategory = table.Column(maxLength: 50, nullable: false) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Authors", x => x.Id); 23 | }); 24 | 25 | migrationBuilder.CreateTable( 26 | name: "Courses", 27 | columns: table => new 28 | { 29 | Id = table.Column(nullable: false), 30 | Title = table.Column(maxLength: 100, nullable: false), 31 | Description = table.Column(maxLength: 1500, nullable: true), 32 | AuthorId = table.Column(nullable: false) 33 | }, 34 | constraints: table => 35 | { 36 | table.PrimaryKey("PK_Courses", x => x.Id); 37 | table.ForeignKey( 38 | name: "FK_Courses_Authors_AuthorId", 39 | column: x => x.AuthorId, 40 | principalTable: "Authors", 41 | principalColumn: "Id", 42 | onDelete: ReferentialAction.Cascade); 43 | }); 44 | 45 | migrationBuilder.InsertData( 46 | table: "Authors", 47 | columns: new[] { "Id", "DateOfBirth", "FirstName", "LastName", "MainCategory" }, 48 | values: new object[,] 49 | { 50 | { 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" }, 51 | { 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" }, 52 | { 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" }, 53 | { 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" }, 54 | { 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" }, 55 | { 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" }, 56 | { 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" }, 57 | { 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" }, 58 | { 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" }, 59 | { 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" }, 60 | { 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" }, 61 | { 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" } 62 | }); 63 | 64 | migrationBuilder.InsertData( 65 | table: "Courses", 66 | columns: new[] { "Id", "AuthorId", "Description", "Title" }, 67 | values: new object[,] 68 | { 69 | { 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" }, 70 | { 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" }, 71 | { 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" }, 72 | { 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" } 73 | }); 74 | 75 | migrationBuilder.CreateIndex( 76 | name: "IX_Courses_AuthorId", 77 | table: "Courses", 78 | column: "AuthorId"); 79 | } 80 | 81 | protected override void Down(MigrationBuilder migrationBuilder) 82 | { 83 | migrationBuilder.DropTable( 84 | name: "Courses"); 85 | 86 | migrationBuilder.DropTable( 87 | name: "Authors"); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Models/AuthorDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.Models 7 | { 8 | public class AuthorDto 9 | { 10 | public Guid Id { get; set; } 11 | public string Name { get; set; } 12 | public int Age { get; set; } 13 | public string MainCategory { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Models/AuthorForCreationDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.Models 7 | { 8 | public class AuthorForCreationDto 9 | { 10 | public string FirstName { get; set; } 11 | public string LastName { get; set; } 12 | public DateTimeOffset DateOfBirth { get; set; } 13 | public string MainCategory { get; set; } 14 | public ICollection Courses { get; set; } 15 | = new List(); 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Models/CourseDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.Models 7 | { 8 | public class CourseDto 9 | { 10 | public Guid Id { get; set; } 11 | 12 | public string Title { get; set; } 13 | 14 | public string Description { get; set; } 15 | 16 | public Guid AuthorId { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Models/CourseForCreationDto.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.ValidationAttributes; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace CourseLibrary.API.Models 9 | { 10 | public class CourseForCreationDto : CourseForManipulationDto 11 | { } 12 | } 13 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Models/CourseForManipulationDto.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.ValidationAttributes; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace CourseLibrary.API.Models 9 | { 10 | [CourseTitleMustBeDifferentFromDescription( 11 | ErrorMessage = "Title must be different from description.")] 12 | public abstract class CourseForManipulationDto 13 | { 14 | [Required(ErrorMessage = "You should fill out a title.")] 15 | [MaxLength(100, ErrorMessage = "The title shouldn't have more than 100 characters.")] 16 | public string Title { get; set; } 17 | 18 | [MaxLength(1500, ErrorMessage = "The description shouldn't have more than 1500 characters.")] 19 | public virtual string Description { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Models/CourseForUpdateDto.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.ValidationAttributes; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace CourseLibrary.API.Models 9 | { 10 | public class CourseForUpdateDto : CourseForManipulationDto 11 | { 12 | [Required(ErrorMessage = "You should fill out a description.")] 13 | public override string Description { get => base.Description; set => base.Description = value; } 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Profiles/AuthorsProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CourseLibrary.API.Helpers; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace CourseLibrary.API.Profiles 9 | { 10 | public class AuthorsProfile : Profile 11 | { 12 | public AuthorsProfile() 13 | { 14 | CreateMap() 15 | .ForMember( 16 | dest => dest.Name, 17 | opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}")) 18 | .ForMember( 19 | dest => dest.Age, 20 | opt => opt.MapFrom(src => src.DateOfBirth.GetCurrentAge())); 21 | 22 | CreateMap(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Profiles/CoursesProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace CourseLibrary.API.Profiles 8 | { 9 | public class CoursesProfile : Profile 10 | { 11 | public CoursesProfile() 12 | { 13 | CreateMap(); 14 | CreateMap(); 15 | CreateMap(); 16 | CreateMap(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Program.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.DbContexts; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | using System; 8 | 9 | namespace CourseLibrary.API 10 | { 11 | public class Program 12 | { 13 | 14 | public static void Main(string[] args) 15 | { 16 | var host = CreateHostBuilder(args).Build(); 17 | 18 | // migrate the database. Best practice = in Main, using service scope 19 | using (var scope = host.Services.CreateScope()) 20 | { 21 | try 22 | { 23 | var context = scope.ServiceProvider.GetService(); 24 | // for demo purposes, delete the database & migrate on startup so 25 | // we can start with a clean slate 26 | context.Database.EnsureDeleted(); 27 | context.Database.Migrate(); 28 | } 29 | catch (Exception ex) 30 | { 31 | var logger = scope.ServiceProvider.GetRequiredService>(); 32 | logger.LogError(ex, "An error occurred while migrating the database."); 33 | } 34 | } 35 | 36 | // run the web app 37 | host.Run(); 38 | } 39 | 40 | 41 | public static IHostBuilder CreateHostBuilder(string[] args) => 42 | Host.CreateDefaultBuilder(args) 43 | .ConfigureWebHostDefaults(webBuilder => 44 | { 45 | webBuilder.UseStartup(); 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iis": { 6 | "applicationUrl": "http://localhost/CourseLibrary.API", 7 | "sslPort": 0 8 | }, 9 | "iisExpress": { 10 | "applicationUrl": "http://localhost:51044", 11 | "sslPort": 0 12 | } 13 | }, 14 | "$schema": "http://json.schemastore.org/launchsettings.json", 15 | "profiles": { 16 | "IIS Express": { 17 | "commandName": "Project", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | }, 21 | "applicationUrl": "http://localhost:51044" 22 | }, 23 | "CourseLibrary.API": { 24 | "commandName": "Project", 25 | "launchBrowser": true, 26 | "launchUrl": "api/values", 27 | "environmentVariables": { 28 | "ASPNETCORE_ENVIRONMENT": "Development" 29 | }, 30 | "applicationUrl": "http://localhost:5000" 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/ResourceParameters/AuthorsResourceParameters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CourseLibrary.API.ResourceParameters 7 | { 8 | public class AuthorsResourceParameters 9 | { 10 | public string MainCategory { get; set; } 11 | public string SearchQuery { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Services/CourseLibraryRepository.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.DbContexts; 2 | using CourseLibrary.API.Entities; 3 | using CourseLibrary.API.ResourceParameters; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace CourseLibrary.API.Services 9 | { 10 | public class CourseLibraryRepository : ICourseLibraryRepository, IDisposable 11 | { 12 | private readonly CourseLibraryContext _context; 13 | 14 | public CourseLibraryRepository(CourseLibraryContext context ) 15 | { 16 | _context = context ?? throw new ArgumentNullException(nameof(context)); 17 | } 18 | 19 | public void AddCourse(Guid authorId, Course course) 20 | { 21 | if (authorId == Guid.Empty) 22 | { 23 | throw new ArgumentNullException(nameof(authorId)); 24 | } 25 | 26 | if (course == null) 27 | { 28 | throw new ArgumentNullException(nameof(course)); 29 | } 30 | // always set the AuthorId to the passed-in authorId 31 | course.AuthorId = authorId; 32 | _context.Courses.Add(course); 33 | } 34 | 35 | public void DeleteCourse(Course course) 36 | { 37 | _context.Courses.Remove(course); 38 | } 39 | 40 | public Course GetCourse(Guid authorId, Guid courseId) 41 | { 42 | if (authorId == Guid.Empty) 43 | { 44 | throw new ArgumentNullException(nameof(authorId)); 45 | } 46 | 47 | if (courseId == Guid.Empty) 48 | { 49 | throw new ArgumentNullException(nameof(courseId)); 50 | } 51 | 52 | return _context.Courses 53 | .Where(c => c.AuthorId == authorId && c.Id == courseId).FirstOrDefault(); 54 | } 55 | 56 | public IEnumerable GetCourses(Guid authorId) 57 | { 58 | if (authorId == Guid.Empty) 59 | { 60 | throw new ArgumentNullException(nameof(authorId)); 61 | } 62 | 63 | return _context.Courses 64 | .Where(c => c.AuthorId == authorId) 65 | .OrderBy(c => c.Title).ToList(); 66 | } 67 | 68 | public void UpdateCourse(Course course) 69 | { 70 | // no code in this implementation 71 | } 72 | 73 | public void AddAuthor(Author author) 74 | { 75 | if (author == null) 76 | { 77 | throw new ArgumentNullException(nameof(author)); 78 | } 79 | 80 | // the repository fills the id (instead of using identity columns) 81 | author.Id = Guid.NewGuid(); 82 | 83 | foreach (var course in author.Courses) 84 | { 85 | course.Id = Guid.NewGuid(); 86 | } 87 | 88 | _context.Authors.Add(author); 89 | } 90 | 91 | public bool AuthorExists(Guid authorId) 92 | { 93 | if (authorId == Guid.Empty) 94 | { 95 | throw new ArgumentNullException(nameof(authorId)); 96 | } 97 | 98 | return _context.Authors.Any(a => a.Id == authorId); 99 | } 100 | 101 | public void DeleteAuthor(Author author) 102 | { 103 | if (author == null) 104 | { 105 | throw new ArgumentNullException(nameof(author)); 106 | } 107 | 108 | _context.Authors.Remove(author); 109 | } 110 | 111 | public Author GetAuthor(Guid authorId) 112 | { 113 | if (authorId == Guid.Empty) 114 | { 115 | throw new ArgumentNullException(nameof(authorId)); 116 | } 117 | 118 | return _context.Authors.FirstOrDefault(a => a.Id == authorId); 119 | } 120 | 121 | public IEnumerable GetAuthors() 122 | { 123 | return _context.Authors.ToList(); 124 | } 125 | 126 | public IEnumerable GetAuthors(AuthorsResourceParameters authorsResourceParameters) 127 | { 128 | if (authorsResourceParameters == null) 129 | { 130 | throw new ArgumentNullException(nameof(authorsResourceParameters)); 131 | } 132 | 133 | if (string.IsNullOrWhiteSpace(authorsResourceParameters.MainCategory) 134 | && string.IsNullOrWhiteSpace(authorsResourceParameters.SearchQuery)) 135 | { 136 | return GetAuthors(); 137 | } 138 | 139 | var collection = _context.Authors as IQueryable; 140 | 141 | if (!string.IsNullOrWhiteSpace(authorsResourceParameters.MainCategory)) 142 | { 143 | var mainCategory = authorsResourceParameters.MainCategory.Trim(); 144 | collection = collection.Where(a => a.MainCategory == mainCategory); 145 | } 146 | 147 | if (!string.IsNullOrWhiteSpace(authorsResourceParameters.SearchQuery)) 148 | { 149 | 150 | var searchQuery = authorsResourceParameters.SearchQuery.Trim(); 151 | collection = collection.Where(a => a.MainCategory.Contains(searchQuery) 152 | || a.FirstName.Contains(searchQuery) 153 | || a.LastName.Contains(searchQuery)); 154 | } 155 | 156 | return collection.ToList(); 157 | } 158 | 159 | public IEnumerable GetAuthors(IEnumerable authorIds) 160 | { 161 | if (authorIds == null) 162 | { 163 | throw new ArgumentNullException(nameof(authorIds)); 164 | } 165 | 166 | return _context.Authors.Where(a => authorIds.Contains(a.Id)) 167 | .OrderBy(a => a.FirstName) 168 | .ThenBy(a => a.LastName) 169 | .ToList(); 170 | } 171 | 172 | public void UpdateAuthor(Author author) 173 | { 174 | // no code in this implementation 175 | } 176 | 177 | public bool Save() 178 | { 179 | return (_context.SaveChanges() >= 0); 180 | } 181 | 182 | public void Dispose() 183 | { 184 | Dispose(true); 185 | GC.SuppressFinalize(this); 186 | } 187 | 188 | protected virtual void Dispose(bool disposing) 189 | { 190 | if (disposing) 191 | { 192 | // dispose resources when needed 193 | } 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Services/ICourseLibraryRepository.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.Entities; 2 | using CourseLibrary.API.ResourceParameters; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace CourseLibrary.API.Services 7 | { 8 | public interface ICourseLibraryRepository 9 | { 10 | IEnumerable GetCourses(Guid authorId); 11 | Course GetCourse(Guid authorId, Guid courseId); 12 | void AddCourse(Guid authorId, Course course); 13 | void UpdateCourse(Course course); 14 | void DeleteCourse(Course course); 15 | IEnumerable GetAuthors(); 16 | IEnumerable GetAuthors(AuthorsResourceParameters authorsResourceParameters); 17 | Author GetAuthor(Guid authorId); 18 | IEnumerable GetAuthors(IEnumerable authorIds); 19 | void AddAuthor(Author author); 20 | void DeleteAuthor(Author author); 21 | void UpdateAuthor(Author author); 22 | bool AuthorExists(Guid authorId); 23 | bool Save(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/Startup.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CourseLibrary.API.DbContexts; 3 | using CourseLibrary.API.Services; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.Infrastructure; 9 | using Microsoft.EntityFrameworkCore; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Hosting; 13 | using Newtonsoft.Json.Serialization; 14 | using System; 15 | 16 | namespace CourseLibrary.API 17 | { 18 | public class Startup 19 | { 20 | public Startup(IConfiguration configuration) 21 | { 22 | Configuration = configuration; 23 | } 24 | 25 | public IConfiguration Configuration { get; } 26 | 27 | // This method gets called by the runtime. Use this method to add services to the container. 28 | public void ConfigureServices(IServiceCollection services) 29 | { 30 | services.AddControllers(setupAction => 31 | { 32 | setupAction.ReturnHttpNotAcceptable = true; 33 | 34 | }).AddNewtonsoftJson(setupAction => 35 | { 36 | setupAction.SerializerSettings.ContractResolver = 37 | new CamelCasePropertyNamesContractResolver(); 38 | }) 39 | .AddXmlDataContractSerializerFormatters() 40 | .ConfigureApiBehaviorOptions(setupAction => 41 | { 42 | setupAction.InvalidModelStateResponseFactory = context => 43 | { 44 | // create a problem details object 45 | var problemDetailsFactory = context.HttpContext.RequestServices 46 | .GetRequiredService(); 47 | var problemDetails = problemDetailsFactory.CreateValidationProblemDetails( 48 | context.HttpContext, 49 | context.ModelState); 50 | 51 | // add additional info not added by default 52 | problemDetails.Detail = "See the errors field for details."; 53 | problemDetails.Instance = context.HttpContext.Request.Path; 54 | 55 | // find out which status code to use 56 | var actionExecutingContext = 57 | context as Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext; 58 | 59 | // if there are modelstate errors & all keys were correctly 60 | // found/parsed we're dealing with validation errors 61 | // 62 | // if the context couldn't be cast to an ActionExecutingContext 63 | // because it's a ControllerContext, we're dealing with an issue 64 | // that happened after the initial input was correctly parsed. 65 | // This happens, for example, when manually validating an object inside 66 | // of a controller action. That means that by then all keys 67 | // WERE correctly found and parsed. In that case, we're 68 | // thus also dealing with a validation error. 69 | if (context.ModelState.ErrorCount > 0 && 70 | (context is ControllerContext || 71 | actionExecutingContext?.ActionArguments.Count == context.ActionDescriptor.Parameters.Count)) 72 | { 73 | problemDetails.Type = "https://courselibrary.com/modelvalidationproblem"; 74 | problemDetails.Status = StatusCodes.Status422UnprocessableEntity; 75 | problemDetails.Title = "One or more validation errors occurred."; 76 | 77 | return new UnprocessableEntityObjectResult(problemDetails) 78 | { 79 | ContentTypes = { "application/problem+json" } 80 | }; 81 | } 82 | 83 | // if one of the keys wasn't correctly found / couldn't be parsed 84 | // we're dealing with null/unparsable input 85 | problemDetails.Status = StatusCodes.Status400BadRequest; 86 | problemDetails.Title = "One or more errors on input occurred."; 87 | return new BadRequestObjectResult(problemDetails) 88 | { 89 | ContentTypes = { "application/problem+json" } 90 | }; 91 | }; 92 | }); 93 | 94 | services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); 95 | 96 | services.AddScoped(); 97 | 98 | services.AddDbContext(options => 99 | { 100 | options.UseSqlServer( 101 | @"Server=(localdb)\mssqllocaldb;Database=CourseLibraryDB;Trusted_Connection=True;"); 102 | }); 103 | } 104 | 105 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 106 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 107 | { 108 | if (env.IsDevelopment()) 109 | { 110 | app.UseDeveloperExceptionPage(); 111 | } 112 | else 113 | { 114 | app.UseExceptionHandler(appBuilder => 115 | { 116 | appBuilder.Run(async context => 117 | { 118 | context.Response.StatusCode = 500; 119 | await context.Response.WriteAsync("An unexpected fault happened. Try again later."); 120 | }); 121 | }); 122 | 123 | } 124 | 125 | app.UseRouting(); 126 | 127 | app.UseAuthorization(); 128 | 129 | app.UseEndpoints(endpoints => 130 | { 131 | endpoints.MapControllers(); 132 | }); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/ValidationAttributes/CourseTitleMustBeDifferentFromDescriptionAttribute.cs: -------------------------------------------------------------------------------- 1 | using CourseLibrary.API.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace CourseLibrary.API.ValidationAttributes 9 | { 10 | public class CourseTitleMustBeDifferentFromDescriptionAttribute : ValidationAttribute 11 | { 12 | protected override ValidationResult IsValid(object value, 13 | ValidationContext validationContext) 14 | { 15 | var course = (CourseForManipulationDto)validationContext.ObjectInstance; 16 | 17 | if (course.Title == course.Description) 18 | { 19 | return new ValidationResult(ErrorMessage, 20 | new[] { nameof(CourseForManipulationDto) }); 21 | } 22 | 23 | return ValidationResult.Success; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /Starter files/CourseLibrary/CourseLibrary.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29009.5 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CourseLibrary.API", "CourseLibrary.API\CourseLibrary.API.csproj", "{2900C008-F2EC-4A0D-8074-F47EB9F84036}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {2900C008-F2EC-4A0D-8074-F47EB9F84036}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {2900C008-F2EC-4A0D-8074-F47EB9F84036}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {2900C008-F2EC-4A0D-8074-F47EB9F84036}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {2900C008-F2EC-4A0D-8074-F47EB9F84036}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {08E0F198-B49E-42B9-9DB8-20DD2062C0CA} 24 | EndGlobalSection 25 | EndGlobal 26 | --------------------------------------------------------------------------------