├── .gitignore ├── Finished sample ├── Library.API.sln └── Library.API │ ├── Attributes │ └── RequestHeaderMatchesMediaTypeAttribute.cs │ ├── Authentication │ └── BasicAuthenticationHandler.cs │ ├── Contexts │ └── LibraryContext.cs │ ├── Controllers │ ├── AuthorsController.cs │ ├── AuthorsControllerV2.cs │ ├── BooksController.cs │ └── ConventionTestsController.cs │ ├── CustomConventions.cs │ ├── EmbeddedAssets │ └── index.html │ ├── Entities │ ├── Author.cs │ └── Book.cs │ ├── IListExtensions.cs │ ├── Library.API.csproj │ ├── Library.API.xml │ ├── LibraryAPI.db │ ├── LibraryAPI.db-shm │ ├── LibraryAPI.db-wal │ ├── Migrations │ ├── InitialMigration.Designer.cs │ ├── InitialMigration.cs │ └── LibraryContextModelSnapshot.cs │ ├── Models │ ├── Author.cs │ ├── AuthorForUpdate.cs │ ├── Book.cs │ ├── BookForCreation.cs │ ├── BookForCreationWithAmountOfPages.cs │ └── BookWithConcatenatedAuthorName.cs │ ├── OperationFilters │ ├── CreateBookOperationFilter.cs │ └── GetBookOperationFilter.cs │ ├── Profiles │ ├── AuthorProfile.cs │ └── BookProfile.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Services │ ├── AuthorRepository.cs │ ├── BookRepository.cs │ ├── IAuthorRepository.cs │ └── IBookRepository.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot │ └── assets │ └── custom-ui.css ├── README.md └── Starter files ├── Library.API.sln └── Library.API ├── Attributes └── RequestHeaderMatchesMediaTypeAttribute.cs ├── Contexts └── LibraryContext.cs ├── Controllers ├── AuthorsController.cs └── BooksController.cs ├── Entities ├── Author.cs └── Book.cs ├── IListExtensions.cs ├── Library.API.csproj ├── LibraryAPI.db ├── LibraryAPI.db-shm ├── LibraryAPI.db-wal ├── Migrations ├── InitialMigration.Designer.cs ├── InitialMigration.cs └── LibraryContextModelSnapshot.cs ├── Models ├── Author.cs ├── AuthorForUpdate.cs ├── Book.cs ├── BookForCreation.cs ├── BookForCreationWithAmountOfPages.cs └── BookWithConcatenatedAuthorName.cs ├── Profiles ├── AuthorProfile.cs └── BookProfile.cs ├── Program.cs ├── Properties └── launchSettings.json ├── Services ├── AuthorRepository.cs ├── BookRepository.cs ├── IAuthorRepository.cs └── IBookRepository.cs ├── appsettings.Development.json └── appsettings.json /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /Finished sample/Library.API.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31919.166 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Library.API", "Library.API\Library.API.csproj", "{981AFB52-5D9D-44F3-A1D9-70A61E09526F}" 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 | {981AFB52-5D9D-44F3-A1D9-70A61E09526F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {981AFB52-5D9D-44F3-A1D9-70A61E09526F}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {981AFB52-5D9D-44F3-A1D9-70A61E09526F}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {981AFB52-5D9D-44F3-A1D9-70A61E09526F}.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 = {8BE4CD99-BAD3-48B4-B950-901EE4C81D70} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Attributes/RequestHeaderMatchesMediaTypeAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.ActionConstraints; 2 | using Microsoft.AspNetCore.Mvc.Formatters; 3 | using Microsoft.Net.Http.Headers; 4 | 5 | namespace Library.API.Attributes 6 | { 7 | [AttributeUsage(AttributeTargets.All, Inherited = true, 8 | AllowMultiple = true)] 9 | public class RequestHeaderMatchesMediaTypeAttribute : Attribute, 10 | IActionConstraint 11 | { 12 | private readonly MediaTypeCollection _mediaTypes = new (); 13 | private readonly string _requestHeaderToMatch; 14 | 15 | public RequestHeaderMatchesMediaTypeAttribute( 16 | string requestHeaderToMatch, 17 | string mediaType, params string[] otherMediaTypes) 18 | { 19 | _requestHeaderToMatch = requestHeaderToMatch 20 | ?? throw new ArgumentNullException(nameof(requestHeaderToMatch)); 21 | 22 | // check if the inputted media types are valid media types 23 | // and add them to the _mediaTypes collection 24 | 25 | if (MediaTypeHeaderValue.TryParse(mediaType, 26 | out MediaTypeHeaderValue? parsedMediaType)) 27 | { 28 | _mediaTypes.Add(parsedMediaType); 29 | } 30 | else 31 | { 32 | throw new ArgumentException("Argument can not be null or empty.", 33 | 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("Argument can not be null or empty.", 46 | nameof(otherMediaTypes)); 47 | } 48 | } 49 | 50 | } 51 | 52 | public int Order { get; } 53 | 54 | public bool Accept(ActionConstraintContext context) 55 | { 56 | var requestHeaders = context.RouteContext.HttpContext 57 | .Request.Headers; 58 | if (!requestHeaders.ContainsKey(_requestHeaderToMatch)) 59 | { 60 | return false; 61 | } 62 | 63 | var parsedRequestMediaType = new MediaType( 64 | requestHeaders[_requestHeaderToMatch]); 65 | 66 | // if one of the media types matches, return true 67 | foreach (var mediaType in _mediaTypes) 68 | { 69 | var parsedMediaType = new MediaType(mediaType); 70 | if (parsedRequestMediaType.Equals(parsedMediaType)) 71 | { 72 | return true; 73 | } 74 | } 75 | return false; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Authentication/BasicAuthenticationHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.Extensions.Options; 3 | using System.Net.Http.Headers; 4 | using System.Security.Claims; 5 | using System.Text; 6 | using System.Text.Encodings.Web; 7 | 8 | namespace Library.API.Authentication 9 | { 10 | public class BasicAuthenticationHandler : 11 | AuthenticationHandler 12 | { 13 | public BasicAuthenticationHandler( 14 | IOptionsMonitor options, 15 | ILoggerFactory logger, 16 | UrlEncoder encoder, 17 | ISystemClock clock) 18 | : base(options, logger, encoder, clock) 19 | { 20 | } 21 | 22 | protected override Task HandleAuthenticateAsync() 23 | { 24 | if (!Request.Headers.ContainsKey("Authorization")) 25 | { 26 | return Task.FromResult( 27 | AuthenticateResult.Fail("Missing Authorization header")); 28 | } 29 | 30 | try 31 | { 32 | var authenticationHeader = AuthenticationHeaderValue.Parse( 33 | Request.Headers["Authorization"]); 34 | var credentialBytes = Convert.FromBase64String(authenticationHeader.Parameter); 35 | var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':'); 36 | var username = credentials[0]; 37 | var password = credentials[1]; 38 | 39 | if (username == "Pluralsight" && password == "Pluralsight") 40 | { 41 | var claims = new[] { 42 | new Claim(ClaimTypes.NameIdentifier, username)}; 43 | var identity = new ClaimsIdentity(claims, Scheme.Name); 44 | var principal = new ClaimsPrincipal(identity); 45 | var ticket = new AuthenticationTicket(principal, Scheme.Name); 46 | 47 | return Task.FromResult(AuthenticateResult.Success(ticket)); 48 | } 49 | return Task.FromResult( 50 | AuthenticateResult.Fail("Invalid username or password")); 51 | } 52 | catch 53 | { 54 | return Task.FromResult( 55 | AuthenticateResult.Fail("Invalid Authorization header")); 56 | } 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Contexts/LibraryContext.cs: -------------------------------------------------------------------------------- 1 | using Library.API.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using System; 4 | 5 | namespace Library.API.Contexts 6 | { 7 | public class LibraryContext : DbContext 8 | { 9 | public DbSet Books { get; set; } = null!; 10 | 11 | public DbSet Authors { get; set; } = null!; 12 | 13 | public LibraryContext(DbContextOptions options) 14 | : base(options) 15 | { 16 | } 17 | 18 | protected override void OnModelCreating(ModelBuilder modelBuilder) 19 | { 20 | // seed the database with dummy data 21 | modelBuilder.Entity().HasData( 22 | new Author() 23 | { 24 | Id = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 25 | FirstName = "George", 26 | LastName = "RR Martin" 27 | }, 28 | new Author() 29 | { 30 | Id = Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 31 | FirstName = "Stephen", 32 | LastName = "Fry" 33 | }, 34 | new Author() 35 | { 36 | Id = Guid.Parse("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"), 37 | FirstName = "James", 38 | LastName = "Elroy" 39 | }, 40 | new Author() 41 | { 42 | Id = Guid.Parse("2902b665-1190-4c70-9915-b9c2d7680450"), 43 | FirstName = "Douglas", 44 | LastName = "Adams" 45 | } 46 | ); 47 | 48 | // seed the database with dummy data 49 | modelBuilder.Entity().HasData( 50 | new Book("A Dance with Dragons") 51 | { 52 | Id = Guid.Parse("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 53 | AuthorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 54 | Description = "A Dance with Dragons is the fifth of seven planned novels in the epic fantasy series A Song of Ice and Fire." 55 | }, 56 | new Book("A Game of Thrones") 57 | { 58 | Id = Guid.Parse("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 59 | AuthorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 60 | Description = "A Game of Thrones is the first novel in A Song of Ice and Fire, a series of fantasy novels by American author George R. R. ... In the novel, recounting events from various points of view, Martin introduces the plot-lines of the noble houses of Westeros, the Wall, and the Targaryens." 61 | }, 62 | new Book("Mythos") 63 | { 64 | Id = Guid.Parse("d173e20d-159e-4127-9ce9-b0ac2564ad97"), 65 | AuthorId = Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 66 | Description = "The Greek myths are amongst the best stories ever told, passed down through millennia and inspiring writers and artists as varied as Shakespeare, Michelangelo, James Joyce and Walt Disney. They are embedded deeply in the traditions, tales and cultural DNA of the West.You'll fall in love with Zeus, marvel at the birth of Athena, wince at Cronus and Gaia's revenge on Ouranos, weep with King Midas and hunt with the beautiful and ferocious Artemis. Spellbinding, informative and moving, Stephen Fry's Mythos perfectly captures these stories for the modern age - in all their rich and deeply human relevance." 67 | }, 68 | new Book("American Tabloid") 69 | { 70 | Id = Guid.Parse("493c3228-3444-4a49-9cc0-e8532edc59b2"), 71 | AuthorId = Guid.Parse("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"), 72 | Description = "American Tabloid is a 1995 novel by James Ellroy that chronicles the events surrounding three rogue American law enforcement officers from November 22, 1958 through November 22, 1963. Each becomes entangled in a web of interconnecting associations between the FBI, the CIA, and the mafia, which eventually leads to their collective involvement in the John F. Kennedy assassination." 73 | }, 74 | new Book("The Hitchhiker's Guide to the Galaxy") 75 | { 76 | Id = Guid.Parse("40ff5488-fdab-45b5-bc3a-14302d59869a"), 77 | AuthorId = Guid.Parse("2902b665-1190-4c70-9915-b9c2d7680450"), 78 | Description = "In The Hitchhiker's Guide to the Galaxy, the characters visit the legendary planet Magrathea, home to the now-collapsed planet-building industry, and meet Slartibartfast, a planetary coastline designer who was responsible for the fjords of Norway. Through archival recordings, he relates the story of a race of hyper-intelligent pan-dimensional beings who built a computer named Deep Thought to calculate the Answer to the Ultimate Question of Life, the Universe, and Everything." 79 | } 80 | ); 81 | 82 | base.OnModelCreating(modelBuilder); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Controllers/AuthorsController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Library.API.Models; 3 | using Library.API.Services; 4 | using Microsoft.AspNetCore.JsonPatch; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Http; 7 | 8 | namespace Library.API.Controllers 9 | { 10 | [Route("api/v{version:apiVersion}/authors")] 11 | [ApiController] 12 | // [ApiExplorerSettings(GroupName = "LibraryOpenAPISpecificationAuthors")] 13 | public class AuthorsController : ControllerBase 14 | { 15 | private readonly IAuthorRepository _authorsRepository; 16 | private readonly IMapper _mapper; 17 | 18 | public AuthorsController( 19 | IAuthorRepository authorsRepository, 20 | IMapper mapper) 21 | { 22 | _authorsRepository = authorsRepository 23 | ?? throw new ArgumentNullException(nameof(authorsRepository)); 24 | _mapper = mapper 25 | ?? throw new ArgumentNullException(nameof(mapper)); 26 | } 27 | 28 | [HttpGet] 29 | public async Task>> GetAuthors() 30 | { 31 | var authorsFromRepo = await _authorsRepository.GetAuthorsAsync(); 32 | return Ok(_mapper.Map>(authorsFromRepo)); 33 | } 34 | 35 | /// 36 | /// Get an author by their id 37 | /// 38 | /// The id of the author you want to get 39 | /// An ActionResult of type Author 40 | [HttpGet("{authorId}")] 41 | public async Task> GetAuthor( 42 | Guid authorId) 43 | { 44 | var authorFromRepo = await _authorsRepository.GetAuthorAsync(authorId); 45 | if (authorFromRepo == null) 46 | { 47 | return NotFound(); 48 | } 49 | 50 | return Ok(_mapper.Map(authorFromRepo)); 51 | } 52 | 53 | [HttpPut("{authorId}")] 54 | [ProducesResponseType(StatusCodes.Status200OK)] 55 | [ProducesResponseType(StatusCodes.Status404NotFound)] 56 | [ProducesDefaultResponseType] 57 | public async Task> UpdateAuthor( 58 | Guid authorId, 59 | AuthorForUpdate authorForUpdate) 60 | { 61 | var authorFromRepo = await _authorsRepository.GetAuthorAsync(authorId); 62 | if (authorFromRepo == null) 63 | { 64 | return NotFound(); 65 | } 66 | 67 | _mapper.Map(authorForUpdate, authorFromRepo); 68 | 69 | // update & save 70 | _authorsRepository.UpdateAuthor(authorFromRepo); 71 | await _authorsRepository.SaveChangesAsync(); 72 | 73 | // return the author 74 | return Ok(_mapper.Map(authorFromRepo)); 75 | } 76 | 77 | /// 78 | /// Partially update an author 79 | /// 80 | /// The id of the author you want to get 81 | /// The set of operations to apply to the author 82 | /// An ActionResult of type Author 83 | /// 84 | /// Sample request (this request updates the author's **first name**): 85 | /// 86 | /// PATCH /authors/authorId 87 | /// [ 88 | /// { 89 | /// "op": "replace", 90 | /// "path": "/firstname", 91 | /// "value": "new first name" 92 | /// } 93 | /// ] 94 | /// 95 | [HttpPatch("{authorId}")] 96 | public async Task> UpdateAuthor( 97 | Guid authorId, 98 | JsonPatchDocument patchDocument) 99 | { 100 | var authorFromRepo = await _authorsRepository.GetAuthorAsync(authorId); 101 | if (authorFromRepo == null) 102 | { 103 | return NotFound(); 104 | } 105 | 106 | // map to DTO to apply the patch to 107 | var author = _mapper.Map(authorFromRepo); 108 | patchDocument.ApplyTo(author, ModelState); 109 | 110 | // if there are errors when applying the patch the patch doc 111 | // was badly formed These aren't caught via the ApiController 112 | // validation, so we must manually check the modelstate and 113 | // potentially return these errors. 114 | if (!ModelState.IsValid) 115 | { 116 | return new UnprocessableEntityObjectResult(ModelState); 117 | } 118 | 119 | // map the applied changes on the DTO back into the entity 120 | _mapper.Map(author, authorFromRepo); 121 | 122 | // update & save 123 | _authorsRepository.UpdateAuthor(authorFromRepo); 124 | await _authorsRepository.SaveChangesAsync(); 125 | 126 | // return the author 127 | return Ok(_mapper.Map(authorFromRepo)); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Controllers/AuthorsControllerV2.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Library.API.Models; 3 | using Library.API.Services; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace Library.API.Controllers 7 | { 8 | [Route("api/v{version:apiVersion}/[controller]")] 9 | [ApiController] 10 | [ApiVersion("2.0")] 11 | public class AuthorsControllerV2 : ControllerBase 12 | { 13 | private readonly IAuthorRepository _authorsRepository; 14 | private readonly IMapper _mapper; 15 | 16 | public AuthorsControllerV2( 17 | IAuthorRepository authorsRepository, 18 | IMapper mapper) 19 | { 20 | _authorsRepository = authorsRepository; 21 | _mapper = mapper; 22 | } 23 | 24 | 25 | /// 26 | /// Get a list of authors, V2 27 | /// 28 | /// 29 | [HttpGet] 30 | [ProducesResponseType(StatusCodes.Status200OK)] 31 | public async Task>> GetAuthors() 32 | { 33 | var authorsFromRepo = await _authorsRepository.GetAuthorsAsync(); 34 | return Ok(_mapper.Map>(authorsFromRepo)); 35 | } 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Controllers/BooksController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Library.API.Models; 3 | using Library.API.Services; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Http; 6 | using Library.API.Attributes; 7 | 8 | namespace Library.API.Controllers 9 | { 10 | [Route("api/v{version:apiVersion}/authors/{authorId}/books")] 11 | [ApiController] 12 | [Produces("application/json", 13 | "application/xml")] 14 | // [ApiExplorerSettings(GroupName = "LibraryOpenAPISpecificationBooks")] 15 | public class BooksController : ControllerBase 16 | { 17 | private readonly IBookRepository _bookRepository; 18 | private readonly IAuthorRepository _authorRepository; 19 | private readonly IMapper _mapper; 20 | 21 | public BooksController( 22 | IBookRepository bookRepository, 23 | IAuthorRepository authorRepository, 24 | IMapper mapper) 25 | { 26 | _bookRepository = bookRepository 27 | ?? throw new ArgumentNullException(nameof(bookRepository)); 28 | _authorRepository = authorRepository 29 | ?? throw new ArgumentNullException(nameof(authorRepository)); 30 | _mapper = mapper 31 | ?? throw new ArgumentNullException(nameof(mapper)); 32 | } 33 | 34 | [HttpGet()] 35 | [ProducesResponseType(StatusCodes.Status200OK)] 36 | [ProducesResponseType(StatusCodes.Status404NotFound)] 37 | [ProducesDefaultResponseType] 38 | public async Task>> GetBooks( 39 | Guid authorId) 40 | { 41 | if (!await _authorRepository.AuthorExistsAsync(authorId)) 42 | { 43 | return NotFound(); 44 | } 45 | 46 | var booksFromRepo = await _bookRepository.GetBooksAsync(authorId); 47 | return Ok(_mapper.Map>(booksFromRepo)); 48 | } 49 | 50 | /// 51 | /// Get a book by id for a specific author 52 | /// 53 | /// The id of the book author 54 | /// The id of the book 55 | /// An ActionResult of type Book 56 | /// Returns the requested book 57 | [RequestHeaderMatchesMediaType("Accept", 58 | "application/json", 59 | "application/vnd.marvin.book+json")] 60 | [Produces("application/vnd.marvin.book+json")] 61 | [ProducesResponseType(StatusCodes.Status404NotFound)] 62 | [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Book))] 63 | [HttpGet("{bookId}", Name = "GetBook")] 64 | public async Task> GetBook( 65 | Guid authorId, 66 | Guid bookId) 67 | { 68 | if (!await _authorRepository.AuthorExistsAsync(authorId)) 69 | { 70 | return NotFound(); 71 | } 72 | 73 | var bookFromRepo = await _bookRepository.GetBookAsync(authorId, bookId); 74 | if (bookFromRepo == null) 75 | { 76 | return NotFound(); 77 | } 78 | 79 | return Ok(_mapper.Map(bookFromRepo)); 80 | } 81 | 82 | /// 83 | /// Get a book by id for a specific author 84 | /// 85 | /// The id of the book author 86 | /// The id of the book 87 | /// An ActionResult of type BookWithConcatenatedAuthorName 88 | [RequestHeaderMatchesMediaType("Accept", 89 | "application/vnd.marvin.bookwithconcatenatedauthorname+json")] 90 | [Produces( 91 | "application/vnd.marvin.bookwithconcatenatedauthorname+json")] 92 | [HttpGet("{bookId}")] 93 | [ProducesResponseType(StatusCodes.Status404NotFound)] 94 | [ProducesResponseType(StatusCodes.Status200OK)] 95 | [ApiExplorerSettings(IgnoreApi = true)] 96 | public async Task> GetBookWithConcatenatedAuthorName( 97 | Guid authorId, 98 | Guid bookId) 99 | { 100 | if (!await _authorRepository.AuthorExistsAsync(authorId)) 101 | { 102 | return NotFound(); 103 | } 104 | 105 | var bookFromRepo = await _bookRepository.GetBookAsync( 106 | authorId, bookId); 107 | if (bookFromRepo == null) 108 | { 109 | return NotFound(); 110 | } 111 | 112 | return Ok(_mapper.Map( 113 | bookFromRepo)); 114 | } 115 | 116 | 117 | [HttpPost(Name ="CreateBook")] 118 | [RequestHeaderMatchesMediaType("Content-Type", 119 | "application/json", 120 | "application/vnd.marvin.bookforcreation+json")] 121 | [Consumes("application/json", 122 | "application/vnd.marvin.bookforcreation+json")] 123 | public async Task> CreateBook( 124 | Guid authorId, 125 | BookForCreation bookForCreation) 126 | { 127 | if (!await _authorRepository.AuthorExistsAsync(authorId)) 128 | { 129 | return NotFound(); 130 | } 131 | 132 | var bookToAdd = _mapper.Map(bookForCreation); 133 | _bookRepository.AddBook(bookToAdd); 134 | await _bookRepository.SaveChangesAsync(); 135 | 136 | return CreatedAtRoute( 137 | "GetBook", 138 | new { authorId, bookId = bookToAdd.Id }, 139 | _mapper.Map(bookToAdd)); 140 | } 141 | 142 | /// 143 | /// Create a book for a specific author 144 | /// 145 | /// The id of the book author 146 | /// The book to create 147 | /// An ActionResult of type Book 148 | /// Validation error 149 | [HttpPost()] 150 | [ProducesResponseType(StatusCodes.Status201Created)] 151 | [ProducesResponseType(StatusCodes.Status404NotFound)] 152 | [ProducesDefaultResponseType] 153 | [RequestHeaderMatchesMediaType("Content-Type", 154 | "application/vnd.marvin.bookforcreationwithamountofpages+json")] 155 | [Consumes("application/vnd.marvin.bookforcreationwithamountofpages+json")] 156 | [ApiExplorerSettings(IgnoreApi = true)] 157 | public async Task> CreateBookWithAmountOfPages( 158 | Guid authorId, 159 | [FromBody] BookForCreationWithAmountOfPages bookForCreationWithAmountOfPages) 160 | { 161 | if (!await _authorRepository.AuthorExistsAsync(authorId)) 162 | { 163 | return NotFound(); 164 | } 165 | 166 | var bookToAdd = _mapper.Map(bookForCreationWithAmountOfPages); 167 | _bookRepository.AddBook(bookToAdd); 168 | await _bookRepository.SaveChangesAsync(); 169 | 170 | return CreatedAtRoute( 171 | "GetBook", 172 | new { authorId, bookId = bookToAdd.Id }, 173 | _mapper.Map(bookToAdd)); 174 | } 175 | 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Controllers/ConventionTestsController.cs: -------------------------------------------------------------------------------- 1 | //using Microsoft.AspNetCore.Mvc; 2 | 3 | //// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 4 | 5 | //namespace Library.API.Controllers 6 | //{ 7 | // [Route("api/[controller]")] 8 | // [ApiController] 9 | // [ApiConventionType(typeof(CustomConventions))] 10 | // public class ConventionTestsController : ControllerBase 11 | // { 12 | // // GET: api/ 13 | // [HttpGet] 14 | // public IEnumerable Get() 15 | // { 16 | // return new string[] { "value1", "value2" }; 17 | // } 18 | 19 | // // GET api//5 20 | // [HttpGet("{id}")] 21 | // //[ApiConventionMethod(typeof(DefaultApiConventions), 22 | // // nameof(DefaultApiConventions.Get))] 23 | // public string Get(int id) 24 | // { 25 | // return "value"; 26 | // } 27 | 28 | // // POST api/ 29 | // [HttpPost] 30 | // //[ApiConventionMethod(typeof(CustomConventions), 31 | // // nameof(CustomConventions.Insert))] 32 | // public void InsertTest([FromBody] string value) 33 | // { 34 | // } 35 | 36 | // // PUT api//5 37 | // [HttpPut("{id}")] 38 | // public void Put(int id, [FromBody] string value) 39 | // { 40 | // } 41 | 42 | // // DELETE api//5 43 | // [HttpDelete("{id}")] 44 | // public void Delete(int id) 45 | // { 46 | // } 47 | // } 48 | //} 49 | -------------------------------------------------------------------------------- /Finished sample/Library.API/CustomConventions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.ApiExplorer; 3 | 4 | namespace Library.API 5 | { 6 | #nullable disable 7 | public static class CustomConventions 8 | { 9 | [ProducesDefaultResponseType] 10 | [ProducesResponseType(StatusCodes.Status201Created)] 11 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 12 | [ApiConventionNameMatch( 13 | ApiConventionNameMatchBehavior.Prefix)] 14 | public static void Insert( 15 | [ApiConventionNameMatch( 16 | ApiConventionNameMatchBehavior.Any)] 17 | [ApiConventionTypeMatch( 18 | ApiConventionTypeMatchBehavior.Any)] 19 | object model) 20 | { } 21 | } 22 | #nullable restore 23 | } 24 | -------------------------------------------------------------------------------- /Finished sample/Library.API/EmbeddedAssets/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | %(DocumentTitle) 7 | 8 | 9 | 10 | 29 | %(HeadContent) 30 | 31 | 32 | 33 |
34 | 35 | 36 | 42 | 43 | 44 | 45 | 116 | 117 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Entities/Author.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | namespace Library.API.Entities 7 | { 8 | #pragma warning disable CS1591 9 | 10 | [Table("Authors")] 11 | public class Author 12 | { 13 | [Key] 14 | public Guid Id { get; set; } 15 | 16 | [Required] 17 | [MaxLength(150)] 18 | public string FirstName { get; set; } = string.Empty; 19 | 20 | [Required] 21 | [MaxLength(150)] 22 | public string LastName { get; set; } = string.Empty; 23 | 24 | public ICollection Books { get; set; } = new List(); 25 | } 26 | #pragma warning restore CS1591 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Entities/Book.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace Library.API.Entities 5 | { 6 | [Table("Books")] 7 | public class Book 8 | { 9 | [Key] 10 | public Guid Id { get; set; } 11 | 12 | [Required] 13 | [MaxLength(150)] 14 | public string Title { get; set; } = string.Empty; 15 | 16 | [MaxLength(2500)] 17 | public string? Description { get; set; } 18 | 19 | public int? AmountOfPages { get; set; } 20 | 21 | public Guid AuthorId { get; set; } 22 | public Author? Author { get; set; } 23 | 24 | public Book(string title) 25 | { 26 | Title = title; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Finished sample/Library.API/IListExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Library.API 5 | { 6 | public static class IListExtensions 7 | { 8 | public static void AddRange(this IList list, IEnumerable items) 9 | { 10 | if (list == null) 11 | { 12 | throw new ArgumentNullException("list"); 13 | } 14 | 15 | if (items == null) 16 | { 17 | throw new ArgumentNullException("items"); 18 | } 19 | 20 | if (list is List) 21 | { 22 | ((List)list).AddRange(items); 23 | } 24 | else 25 | { 26 | foreach (var item in items) 27 | { 28 | list.Add(item); 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Library.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | True 8 | Library.API.xml 9 | true 10 | 11 | 12 | 13 | False 14 | 1701;1702;1591 15 | 16 | 17 | 18 | False 19 | 1701;1702;1591 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | all 40 | runtime; build; native; contentfiles; analyzers; buildtransitive 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Library.API.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library.API 5 | 6 | 7 | 8 | 9 | Get an author by their id 10 | 11 | The id of the author you want to get 12 | An ActionResult of type Author 13 | 14 | 15 | 16 | Partially update an author 17 | 18 | The id of the author you want to get 19 | The set of operations to apply to the author 20 | An ActionResult of type Author 21 | 22 | Sample request (this request updates the author's **first name**): 23 | 24 | PATCH /authors/authorId 25 | [ 26 | { 27 | "op": "replace", 28 | "path": "/firstname", 29 | "value": "new first name" 30 | } 31 | ] 32 | 33 | 34 | 35 | 36 | Get a list of authors, V2 37 | 38 | 39 | 40 | 41 | 42 | Get a book by id for a specific author 43 | 44 | The id of the book author 45 | The id of the book 46 | An ActionResult of type Book 47 | Returns the requested book 48 | 49 | 50 | 51 | Get a book by id for a specific author 52 | 53 | The id of the book author 54 | The id of the book 55 | An ActionResult of type BookWithConcatenatedAuthorName 56 | 57 | 58 | 59 | Create a book for a specific author 60 | 61 | The id of the book author 62 | The book to create 63 | An ActionResult of type Book 64 | Validation error 65 | 66 | 67 | 68 | An author with Id, FirstName and LastName fields 69 | 70 | 71 | 72 | 73 | The id of the author 74 | 75 | 76 | 77 | 78 | The first name of the 79 | **author** 80 | 81 | 82 | 83 | 84 | The last name of the author 85 | 86 | 87 | 88 | 89 | An author for update with FirstName and LastName fields 90 | 91 | 92 | 93 | 94 | The first name of the author 95 | 96 | 97 | 98 | 99 | The last name of the author 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /Finished sample/Library.API/LibraryAPI.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinDockx/DocumentingAspNetCore6API/346a2cd0a181da1091a1c0f60db836bde56a488c/Finished sample/Library.API/LibraryAPI.db -------------------------------------------------------------------------------- /Finished sample/Library.API/LibraryAPI.db-shm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinDockx/DocumentingAspNetCore6API/346a2cd0a181da1091a1c0f60db836bde56a488c/Finished sample/Library.API/LibraryAPI.db-shm -------------------------------------------------------------------------------- /Finished sample/Library.API/LibraryAPI.db-wal: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinDockx/DocumentingAspNetCore6API/346a2cd0a181da1091a1c0f60db836bde56a488c/Finished sample/Library.API/LibraryAPI.db-wal -------------------------------------------------------------------------------- /Finished sample/Library.API/Migrations/InitialMigration.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Library.API.Contexts; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | #nullable disable 10 | 11 | namespace Library.API.Migrations 12 | { 13 | [DbContext(typeof(LibraryContext))] 14 | [Migration("20220316134041_InitialMigration")] 15 | partial class InitialMigration 16 | { 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder.HasAnnotation("ProductVersion", "6.0.3"); 21 | 22 | modelBuilder.Entity("Library.API.Entities.Author", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd() 26 | .HasColumnType("TEXT"); 27 | 28 | b.Property("FirstName") 29 | .IsRequired() 30 | .HasMaxLength(150) 31 | .HasColumnType("TEXT"); 32 | 33 | b.Property("LastName") 34 | .IsRequired() 35 | .HasMaxLength(150) 36 | .HasColumnType("TEXT"); 37 | 38 | b.HasKey("Id"); 39 | 40 | b.ToTable("Authors"); 41 | 42 | b.HasData( 43 | new 44 | { 45 | Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 46 | FirstName = "George", 47 | LastName = "RR Martin" 48 | }, 49 | new 50 | { 51 | Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 52 | FirstName = "Stephen", 53 | LastName = "Fry" 54 | }, 55 | new 56 | { 57 | Id = new Guid("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"), 58 | FirstName = "James", 59 | LastName = "Elroy" 60 | }, 61 | new 62 | { 63 | Id = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 64 | FirstName = "Douglas", 65 | LastName = "Adams" 66 | }); 67 | }); 68 | 69 | modelBuilder.Entity("Library.API.Entities.Book", b => 70 | { 71 | b.Property("Id") 72 | .ValueGeneratedOnAdd() 73 | .HasColumnType("TEXT"); 74 | 75 | b.Property("AmountOfPages") 76 | .HasColumnType("INTEGER"); 77 | 78 | b.Property("AuthorId") 79 | .HasColumnType("TEXT"); 80 | 81 | b.Property("Description") 82 | .HasMaxLength(2500) 83 | .HasColumnType("TEXT"); 84 | 85 | b.Property("Title") 86 | .IsRequired() 87 | .HasMaxLength(150) 88 | .HasColumnType("TEXT"); 89 | 90 | b.HasKey("Id"); 91 | 92 | b.HasIndex("AuthorId"); 93 | 94 | b.ToTable("Books"); 95 | 96 | b.HasData( 97 | new 98 | { 99 | Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 100 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 101 | Description = "A Dance with Dragons is the fifth of seven planned novels in the epic fantasy series A Song of Ice and Fire.", 102 | Title = "A Dance with Dragons" 103 | }, 104 | new 105 | { 106 | Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 107 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 108 | Description = "A Game of Thrones is the first novel in A Song of Ice and Fire, a series of fantasy novels by American author George R. R. ... In the novel, recounting events from various points of view, Martin introduces the plot-lines of the noble houses of Westeros, the Wall, and the Targaryens.", 109 | Title = "A Game of Thrones" 110 | }, 111 | new 112 | { 113 | Id = new Guid("d173e20d-159e-4127-9ce9-b0ac2564ad97"), 114 | AuthorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 115 | Description = "The Greek myths are amongst the best stories ever told, passed down through millennia and inspiring writers and artists as varied as Shakespeare, Michelangelo, James Joyce and Walt Disney. They are embedded deeply in the traditions, tales and cultural DNA of the West.You'll fall in love with Zeus, marvel at the birth of Athena, wince at Cronus and Gaia's revenge on Ouranos, weep with King Midas and hunt with the beautiful and ferocious Artemis. Spellbinding, informative and moving, Stephen Fry's Mythos perfectly captures these stories for the modern age - in all their rich and deeply human relevance.", 116 | Title = "Mythos" 117 | }, 118 | new 119 | { 120 | Id = new Guid("493c3228-3444-4a49-9cc0-e8532edc59b2"), 121 | AuthorId = new Guid("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"), 122 | Description = "American Tabloid is a 1995 novel by James Ellroy that chronicles the events surrounding three rogue American law enforcement officers from November 22, 1958 through November 22, 1963. Each becomes entangled in a web of interconnecting associations between the FBI, the CIA, and the mafia, which eventually leads to their collective involvement in the John F. Kennedy assassination.", 123 | Title = "American Tabloid" 124 | }, 125 | new 126 | { 127 | Id = new Guid("40ff5488-fdab-45b5-bc3a-14302d59869a"), 128 | AuthorId = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 129 | Description = "In The Hitchhiker's Guide to the Galaxy, the characters visit the legendary planet Magrathea, home to the now-collapsed planet-building industry, and meet Slartibartfast, a planetary coastline designer who was responsible for the fjords of Norway. Through archival recordings, he relates the story of a race of hyper-intelligent pan-dimensional beings who built a computer named Deep Thought to calculate the Answer to the Ultimate Question of Life, the Universe, and Everything.", 130 | Title = "The Hitchhiker's Guide to the Galaxy" 131 | }); 132 | }); 133 | 134 | modelBuilder.Entity("Library.API.Entities.Book", b => 135 | { 136 | b.HasOne("Library.API.Entities.Author", "Author") 137 | .WithMany("Books") 138 | .HasForeignKey("AuthorId") 139 | .OnDelete(DeleteBehavior.Cascade) 140 | .IsRequired(); 141 | 142 | b.Navigation("Author"); 143 | }); 144 | 145 | modelBuilder.Entity("Library.API.Entities.Author", b => 146 | { 147 | b.Navigation("Books"); 148 | }); 149 | #pragma warning restore 612, 618 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Migrations/InitialMigration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace Library.API.Migrations 7 | { 8 | public partial class InitialMigration : Migration 9 | { 10 | protected override void Up(MigrationBuilder migrationBuilder) 11 | { 12 | migrationBuilder.CreateTable( 13 | name: "Authors", 14 | columns: table => new 15 | { 16 | Id = table.Column(type: "TEXT", nullable: false), 17 | FirstName = table.Column(type: "TEXT", maxLength: 150, nullable: false), 18 | LastName = table.Column(type: "TEXT", maxLength: 150, nullable: false) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Authors", x => x.Id); 23 | }); 24 | 25 | migrationBuilder.CreateTable( 26 | name: "Books", 27 | columns: table => new 28 | { 29 | Id = table.Column(type: "TEXT", nullable: false), 30 | Title = table.Column(type: "TEXT", maxLength: 150, nullable: false), 31 | Description = table.Column(type: "TEXT", maxLength: 2500, nullable: true), 32 | AmountOfPages = table.Column(type: "INTEGER", nullable: true), 33 | AuthorId = table.Column(type: "TEXT", nullable: false) 34 | }, 35 | constraints: table => 36 | { 37 | table.PrimaryKey("PK_Books", x => x.Id); 38 | table.ForeignKey( 39 | name: "FK_Books_Authors_AuthorId", 40 | column: x => x.AuthorId, 41 | principalTable: "Authors", 42 | principalColumn: "Id", 43 | onDelete: ReferentialAction.Cascade); 44 | }); 45 | 46 | migrationBuilder.InsertData( 47 | table: "Authors", 48 | columns: new[] { "Id", "FirstName", "LastName" }, 49 | values: new object[] { new Guid("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"), "James", "Elroy" }); 50 | 51 | migrationBuilder.InsertData( 52 | table: "Authors", 53 | columns: new[] { "Id", "FirstName", "LastName" }, 54 | values: new object[] { new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), "Douglas", "Adams" }); 55 | 56 | migrationBuilder.InsertData( 57 | table: "Authors", 58 | columns: new[] { "Id", "FirstName", "LastName" }, 59 | values: new object[] { new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), "George", "RR Martin" }); 60 | 61 | migrationBuilder.InsertData( 62 | table: "Authors", 63 | columns: new[] { "Id", "FirstName", "LastName" }, 64 | values: new object[] { new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), "Stephen", "Fry" }); 65 | 66 | migrationBuilder.InsertData( 67 | table: "Books", 68 | columns: new[] { "Id", "AmountOfPages", "AuthorId", "Description", "Title" }, 69 | values: new object[] { new Guid("40ff5488-fdab-45b5-bc3a-14302d59869a"), null, new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), "In The Hitchhiker's Guide to the Galaxy, the characters visit the legendary planet Magrathea, home to the now-collapsed planet-building industry, and meet Slartibartfast, a planetary coastline designer who was responsible for the fjords of Norway. Through archival recordings, he relates the story of a race of hyper-intelligent pan-dimensional beings who built a computer named Deep Thought to calculate the Answer to the Ultimate Question of Life, the Universe, and Everything.", "The Hitchhiker's Guide to the Galaxy" }); 70 | 71 | migrationBuilder.InsertData( 72 | table: "Books", 73 | columns: new[] { "Id", "AmountOfPages", "AuthorId", "Description", "Title" }, 74 | values: new object[] { new Guid("493c3228-3444-4a49-9cc0-e8532edc59b2"), null, new Guid("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"), "American Tabloid is a 1995 novel by James Ellroy that chronicles the events surrounding three rogue American law enforcement officers from November 22, 1958 through November 22, 1963. Each becomes entangled in a web of interconnecting associations between the FBI, the CIA, and the mafia, which eventually leads to their collective involvement in the John F. Kennedy assassination.", "American Tabloid" }); 75 | 76 | migrationBuilder.InsertData( 77 | table: "Books", 78 | columns: new[] { "Id", "AmountOfPages", "AuthorId", "Description", "Title" }, 79 | values: new object[] { new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), null, new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), "A Dance with Dragons is the fifth of seven planned novels in the epic fantasy series A Song of Ice and Fire.", "A Dance with Dragons" }); 80 | 81 | migrationBuilder.InsertData( 82 | table: "Books", 83 | columns: new[] { "Id", "AmountOfPages", "AuthorId", "Description", "Title" }, 84 | values: new object[] { new Guid("d173e20d-159e-4127-9ce9-b0ac2564ad97"), null, new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), "The Greek myths are amongst the best stories ever told, passed down through millennia and inspiring writers and artists as varied as Shakespeare, Michelangelo, James Joyce and Walt Disney. They are embedded deeply in the traditions, tales and cultural DNA of the West.You'll fall in love with Zeus, marvel at the birth of Athena, wince at Cronus and Gaia's revenge on Ouranos, weep with King Midas and hunt with the beautiful and ferocious Artemis. Spellbinding, informative and moving, Stephen Fry's Mythos perfectly captures these stories for the modern age - in all their rich and deeply human relevance.", "Mythos" }); 85 | 86 | migrationBuilder.InsertData( 87 | table: "Books", 88 | columns: new[] { "Id", "AmountOfPages", "AuthorId", "Description", "Title" }, 89 | values: new object[] { new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), null, new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), "A Game of Thrones is the first novel in A Song of Ice and Fire, a series of fantasy novels by American author George R. R. ... In the novel, recounting events from various points of view, Martin introduces the plot-lines of the noble houses of Westeros, the Wall, and the Targaryens.", "A Game of Thrones" }); 90 | 91 | migrationBuilder.CreateIndex( 92 | name: "IX_Books_AuthorId", 93 | table: "Books", 94 | column: "AuthorId"); 95 | } 96 | 97 | protected override void Down(MigrationBuilder migrationBuilder) 98 | { 99 | migrationBuilder.DropTable( 100 | name: "Books"); 101 | 102 | migrationBuilder.DropTable( 103 | name: "Authors"); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Migrations/LibraryContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Library.API.Contexts; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | 8 | #nullable disable 9 | 10 | namespace Library.API.Migrations 11 | { 12 | [DbContext(typeof(LibraryContext))] 13 | partial class LibraryContextModelSnapshot : ModelSnapshot 14 | { 15 | protected override void BuildModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder.HasAnnotation("ProductVersion", "6.0.3"); 19 | 20 | modelBuilder.Entity("Library.API.Entities.Author", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd() 24 | .HasColumnType("TEXT"); 25 | 26 | b.Property("FirstName") 27 | .IsRequired() 28 | .HasMaxLength(150) 29 | .HasColumnType("TEXT"); 30 | 31 | b.Property("LastName") 32 | .IsRequired() 33 | .HasMaxLength(150) 34 | .HasColumnType("TEXT"); 35 | 36 | b.HasKey("Id"); 37 | 38 | b.ToTable("Authors"); 39 | 40 | b.HasData( 41 | new 42 | { 43 | Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 44 | FirstName = "George", 45 | LastName = "RR Martin" 46 | }, 47 | new 48 | { 49 | Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 50 | FirstName = "Stephen", 51 | LastName = "Fry" 52 | }, 53 | new 54 | { 55 | Id = new Guid("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"), 56 | FirstName = "James", 57 | LastName = "Elroy" 58 | }, 59 | new 60 | { 61 | Id = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 62 | FirstName = "Douglas", 63 | LastName = "Adams" 64 | }); 65 | }); 66 | 67 | modelBuilder.Entity("Library.API.Entities.Book", b => 68 | { 69 | b.Property("Id") 70 | .ValueGeneratedOnAdd() 71 | .HasColumnType("TEXT"); 72 | 73 | b.Property("AmountOfPages") 74 | .HasColumnType("INTEGER"); 75 | 76 | b.Property("AuthorId") 77 | .HasColumnType("TEXT"); 78 | 79 | b.Property("Description") 80 | .HasMaxLength(2500) 81 | .HasColumnType("TEXT"); 82 | 83 | b.Property("Title") 84 | .IsRequired() 85 | .HasMaxLength(150) 86 | .HasColumnType("TEXT"); 87 | 88 | b.HasKey("Id"); 89 | 90 | b.HasIndex("AuthorId"); 91 | 92 | b.ToTable("Books"); 93 | 94 | b.HasData( 95 | new 96 | { 97 | Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 98 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 99 | Description = "A Dance with Dragons is the fifth of seven planned novels in the epic fantasy series A Song of Ice and Fire.", 100 | Title = "A Dance with Dragons" 101 | }, 102 | new 103 | { 104 | Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 105 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 106 | Description = "A Game of Thrones is the first novel in A Song of Ice and Fire, a series of fantasy novels by American author George R. R. ... In the novel, recounting events from various points of view, Martin introduces the plot-lines of the noble houses of Westeros, the Wall, and the Targaryens.", 107 | Title = "A Game of Thrones" 108 | }, 109 | new 110 | { 111 | Id = new Guid("d173e20d-159e-4127-9ce9-b0ac2564ad97"), 112 | AuthorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 113 | Description = "The Greek myths are amongst the best stories ever told, passed down through millennia and inspiring writers and artists as varied as Shakespeare, Michelangelo, James Joyce and Walt Disney. They are embedded deeply in the traditions, tales and cultural DNA of the West.You'll fall in love with Zeus, marvel at the birth of Athena, wince at Cronus and Gaia's revenge on Ouranos, weep with King Midas and hunt with the beautiful and ferocious Artemis. Spellbinding, informative and moving, Stephen Fry's Mythos perfectly captures these stories for the modern age - in all their rich and deeply human relevance.", 114 | Title = "Mythos" 115 | }, 116 | new 117 | { 118 | Id = new Guid("493c3228-3444-4a49-9cc0-e8532edc59b2"), 119 | AuthorId = new Guid("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"), 120 | Description = "American Tabloid is a 1995 novel by James Ellroy that chronicles the events surrounding three rogue American law enforcement officers from November 22, 1958 through November 22, 1963. Each becomes entangled in a web of interconnecting associations between the FBI, the CIA, and the mafia, which eventually leads to their collective involvement in the John F. Kennedy assassination.", 121 | Title = "American Tabloid" 122 | }, 123 | new 124 | { 125 | Id = new Guid("40ff5488-fdab-45b5-bc3a-14302d59869a"), 126 | AuthorId = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 127 | Description = "In The Hitchhiker's Guide to the Galaxy, the characters visit the legendary planet Magrathea, home to the now-collapsed planet-building industry, and meet Slartibartfast, a planetary coastline designer who was responsible for the fjords of Norway. Through archival recordings, he relates the story of a race of hyper-intelligent pan-dimensional beings who built a computer named Deep Thought to calculate the Answer to the Ultimate Question of Life, the Universe, and Everything.", 128 | Title = "The Hitchhiker's Guide to the Galaxy" 129 | }); 130 | }); 131 | 132 | modelBuilder.Entity("Library.API.Entities.Book", b => 133 | { 134 | b.HasOne("Library.API.Entities.Author", "Author") 135 | .WithMany("Books") 136 | .HasForeignKey("AuthorId") 137 | .OnDelete(DeleteBehavior.Cascade) 138 | .IsRequired(); 139 | 140 | b.Navigation("Author"); 141 | }); 142 | 143 | modelBuilder.Entity("Library.API.Entities.Author", b => 144 | { 145 | b.Navigation("Books"); 146 | }); 147 | #pragma warning restore 612, 618 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Models/Author.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Library.API.Models 4 | { 5 | 6 | /// 7 | /// An author with Id, FirstName and LastName fields 8 | /// 9 | public class Author 10 | { 11 | /// 12 | /// The id of the author 13 | /// 14 | public Guid Id { get; set; } 15 | 16 | /// 17 | /// The first name of the 18 | /// **author** 19 | /// 20 | public string? FirstName { get; set; } 21 | 22 | /// 23 | /// The last name of the author 24 | /// 25 | public string? LastName { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Models/AuthorForUpdate.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Library.API.Models 4 | { 5 | 6 | /// 7 | /// An author for update with FirstName and LastName fields 8 | /// 9 | public class AuthorForUpdate 10 | { 11 | /// 12 | /// The first name of the author 13 | /// 14 | [Required(ErrorMessage = "The author's first name is required.")] 15 | [MaxLength(150)] 16 | public string? FirstName { get; set; } 17 | 18 | /// 19 | /// The last name of the author 20 | /// 21 | [Required(ErrorMessage = "The author's last name is required.")] 22 | [MaxLength(150)] 23 | public string? LastName { get; set; } 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Models/Book.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Library.API.Models 4 | { 5 | public class Book 6 | { 7 | public Guid Id { get; set; } 8 | 9 | public string? AuthorFirstName { get; set; } 10 | 11 | public string? AuthorLastName { get; set; } 12 | 13 | public string? Title { get; set; } 14 | 15 | public string? Description { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Models/BookForCreation.cs: -------------------------------------------------------------------------------- 1 | namespace Library.API.Models 2 | { 3 | public class BookForCreation 4 | { 5 | public string? Title { get; set; } 6 | 7 | public string? Description { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Models/BookForCreationWithAmountOfPages.cs: -------------------------------------------------------------------------------- 1 | namespace Library.API.Models 2 | { 3 | public class BookForCreationWithAmountOfPages : BookForCreation 4 | { 5 | public int AmountOfPages { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Models/BookWithConcatenatedAuthorName.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Library.API.Models 4 | { 5 | public class BookWithConcatenatedAuthorName 6 | { 7 | public Guid Id { get; set; } 8 | 9 | public string? Author { get; set; } 10 | 11 | public string? Title { get; set; } 12 | 13 | public string? Description { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Finished sample/Library.API/OperationFilters/CreateBookOperationFilter.cs: -------------------------------------------------------------------------------- 1 | using Library.API.Models; 2 | using Microsoft.OpenApi.Models; 3 | using Swashbuckle.AspNetCore.SwaggerGen; 4 | 5 | namespace Library.API.OperationFilters 6 | { 7 | public class CreateBookOperationFilter : IOperationFilter 8 | { 9 | public void Apply(OpenApiOperation operation, 10 | OperationFilterContext context) 11 | { 12 | if (operation.OperationId != "CreateBook") 13 | { 14 | return; 15 | } 16 | operation.RequestBody.Content.Add( 17 | "application/vnd.marvin.bookforcreationwithamountofpages+json", 18 | new OpenApiMediaType() 19 | { 20 | Schema = context.SchemaGenerator.GenerateSchema( 21 | typeof(BookForCreationWithAmountOfPages), 22 | context.SchemaRepository) 23 | }); 24 | 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Finished sample/Library.API/OperationFilters/GetBookOperationFilter.cs: -------------------------------------------------------------------------------- 1 | using Library.API.Models; 2 | using Microsoft.OpenApi.Models; 3 | using Swashbuckle.AspNetCore.SwaggerGen; 4 | 5 | namespace Library.API.OperationFilters 6 | { 7 | public class GetBookOperationFilter : IOperationFilter 8 | { 9 | public void Apply(OpenApiOperation operation, 10 | OperationFilterContext context) 11 | { 12 | if (operation.OperationId != "GetBook") 13 | { 14 | return; 15 | } 16 | if (operation.Responses.Any(response => 17 | response.Key == StatusCodes.Status200OK.ToString())) 18 | { 19 | var schema = context.SchemaGenerator.GenerateSchema( 20 | typeof(BookWithConcatenatedAuthorName), 21 | context.SchemaRepository); 22 | 23 | operation.Responses[StatusCodes.Status200OK.ToString()] 24 | .Content.Add( 25 | "application/vnd.marvin.bookwithconcatenatedauthorname+json", 26 | new OpenApiMediaType() { Schema = schema }); 27 | 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Finished sample/Library.API/Profiles/AuthorProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace Library.API.Profiles 4 | { 5 | public class AuthorProfile : Profile 6 | { 7 | public AuthorProfile() 8 | { 9 | CreateMap(); 10 | 11 | CreateMap(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Profiles/BookProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace Library.API.Profiles 4 | { 5 | public class BookProfile : Profile 6 | { 7 | public BookProfile() 8 | { 9 | CreateMap() 10 | .ForMember(dest => dest.Author, opt => opt.MapFrom(src => 11 | src.Author == null ? "" : $"{src.Author.FirstName} {src.Author.LastName}") 12 | ); 13 | 14 | CreateMap() 15 | .ForMember(dest => dest.AuthorFirstName, opt => opt.MapFrom(src => 16 | src.Author == null ? "" : $"{src.Author.FirstName}")) 17 | .ForMember(dest => dest.AuthorLastName, opt => opt.MapFrom(src => 18 | src.Author == null ? "" : $"{src.Author.LastName}")); 19 | 20 | CreateMap(); 21 | 22 | CreateMap(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Program.cs: -------------------------------------------------------------------------------- 1 | using Library.API; 2 | using Library.API.Authentication; 3 | using Library.API.Contexts; 4 | using Library.API.OperationFilters; 5 | using Library.API.Services; 6 | using Microsoft.AspNetCore.Authentication; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.Abstractions; 9 | using Microsoft.AspNetCore.Mvc.ApiExplorer; 10 | using Microsoft.AspNetCore.Mvc.Authorization; 11 | using Microsoft.AspNetCore.Mvc.Formatters; 12 | using Microsoft.AspNetCore.Mvc.Versioning; 13 | using Microsoft.EntityFrameworkCore; 14 | using Microsoft.OpenApi.Models; 15 | using Newtonsoft.Json.Serialization; 16 | using Swashbuckle.AspNetCore.SwaggerUI; 17 | using System.Reflection; 18 | 19 | [assembly: ApiConventionType(typeof(DefaultApiConventions))] 20 | 21 | var builder = WebApplication.CreateBuilder(args); 22 | 23 | // Add services to the container. 24 | builder.Services.AddControllers(configure => 25 | { 26 | configure.ReturnHttpNotAcceptable = true; 27 | 28 | configure.Filters.Add( 29 | new ProducesResponseTypeAttribute( 30 | StatusCodes.Status400BadRequest)); 31 | configure.Filters.Add( 32 | new ProducesResponseTypeAttribute( 33 | StatusCodes.Status406NotAcceptable)); 34 | configure.Filters.Add( 35 | new ProducesResponseTypeAttribute( 36 | StatusCodes.Status500InternalServerError)); 37 | configure.Filters.Add( 38 | new ProducesResponseTypeAttribute( 39 | StatusCodes.Status401Unauthorized)); 40 | 41 | configure.Filters.Add(new AuthorizeFilter()); 42 | 43 | }).AddNewtonsoftJson(setupAction => 44 | { 45 | setupAction.SerializerSettings.ContractResolver = 46 | new CamelCasePropertyNamesContractResolver(); 47 | }).AddXmlDataContractSerializerFormatters(); 48 | 49 | // configure the NewtonsoftJsonOutputFormatter 50 | builder.Services.Configure(configureOptions => 51 | { 52 | var jsonOutputFormatter = configureOptions.OutputFormatters 53 | .OfType().FirstOrDefault(); 54 | 55 | if (jsonOutputFormatter != null) 56 | { 57 | // remove text/json as it isn't the approved media type 58 | // for working with JSON at API level 59 | if (jsonOutputFormatter.SupportedMediaTypes.Contains("text/json")) 60 | { 61 | jsonOutputFormatter.SupportedMediaTypes.Remove("text/json"); 62 | } 63 | } 64 | }); 65 | 66 | builder.Services.AddDbContext( 67 | dbContextOptions => dbContextOptions.UseSqlite( 68 | builder.Configuration["ConnectionStrings:LibraryDBConnectionString"])); 69 | 70 | builder.Services.AddScoped(); 71 | builder.Services.AddScoped(); 72 | 73 | builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); 74 | 75 | builder.Services.AddApiVersioning(setupAction => 76 | { 77 | setupAction.AssumeDefaultVersionWhenUnspecified = true; 78 | setupAction.DefaultApiVersion = new ApiVersion(1, 0); 79 | setupAction.ReportApiVersions = true; 80 | 81 | }); 82 | 83 | builder.Services.AddVersionedApiExplorer(setupAction => 84 | { 85 | setupAction.GroupNameFormat = "'v'VV"; 86 | }); 87 | 88 | var apiVersionDescriptionProvider = 89 | builder.Services.BuildServiceProvider() 90 | .GetService(); 91 | 92 | builder.Services.AddSwaggerGen(setupAction => 93 | { 94 | setupAction.AddSecurityDefinition("basicAuth", 95 | new OpenApiSecurityScheme() 96 | { 97 | Type = SecuritySchemeType.Http, 98 | Scheme = "basic", 99 | Description = "Input your username and password to access this API" 100 | }); 101 | 102 | setupAction.AddSecurityRequirement( 103 | new OpenApiSecurityRequirement 104 | { 105 | { 106 | new OpenApiSecurityScheme 107 | { 108 | Reference = new OpenApiReference { 109 | Type = ReferenceType.SecurityScheme, 110 | Id = "basicAuth" } 111 | }, new List() 112 | } 113 | }); 114 | 115 | foreach (var description in 116 | apiVersionDescriptionProvider.ApiVersionDescriptions) 117 | { 118 | setupAction.SwaggerDoc( 119 | $"LibraryOpenAPISpecification{description.GroupName}", new() 120 | { 121 | Title = "Library API", 122 | Version = description.ApiVersion.ToString(), 123 | Description = "Through this API you can access authors and their books.", 124 | Contact = new() 125 | { 126 | Email = "kevin.dockx@gmail.com", 127 | Name = "Kevin Dockx", 128 | Url = new Uri("https://www.twitter.com/KevinDockx") 129 | }, 130 | License = new() 131 | { 132 | Name = "MIT License", 133 | Url = new Uri("https://opensource.org/licenses/MIT") 134 | } 135 | }); 136 | } 137 | 138 | setupAction.DocInclusionPredicate((documentName, apiDescription) 139 | => 140 | { 141 | var actionApiVersionModel = apiDescription.ActionDescriptor 142 | .GetApiVersionModel( 143 | ApiVersionMapping.Explicit | ApiVersionMapping.Implicit); 144 | 145 | if (actionApiVersionModel == null) 146 | { 147 | return true; 148 | } 149 | 150 | if (actionApiVersionModel.DeclaredApiVersions.Any()) 151 | { 152 | return actionApiVersionModel.DeclaredApiVersions.Any(v => 153 | $"LibraryOpenAPISpecificationv{v}" == documentName); 154 | } 155 | return actionApiVersionModel.ImplementedApiVersions.Any(v => 156 | $"LibraryOpenAPISpecificationv{v}" == documentName); 157 | }); 158 | 159 | 160 | //setupAction.SwaggerDoc("LibraryOpenAPISpecificationAuthors", new() 161 | //{ 162 | // Title = "Library API (Authors)", 163 | // Version = "1", 164 | // Description = "Through this API you can access authors", 165 | // Contact = new() 166 | // { 167 | // Email = "kevin.dockx@gmail.com", 168 | // Name = "Kevin Dockx", 169 | // Url = new Uri("https://www.twitter.com/KevinDockx") 170 | // }, 171 | // License = new() 172 | // { 173 | // Name = "MIT License", 174 | // Url = new Uri("https://opensource.org/licenses/MIT") 175 | // } 176 | //}); 177 | 178 | //setupAction.SwaggerDoc("LibraryOpenAPISpecificationBooks", new() 179 | //{ 180 | // Title = "Library API (Books)", 181 | // Version = "1", 182 | // Description = "Through this API you can access books", 183 | // Contact = new() 184 | // { 185 | // Email = "kevin.dockx@gmail.com", 186 | // Name = "Kevin Dockx", 187 | // Url = new Uri("https://www.twitter.com/KevinDockx") 188 | // }, 189 | // License = new() 190 | // { 191 | // Name = "MIT License", 192 | // Url = new Uri("https://opensource.org/licenses/MIT") 193 | // } 194 | //}); 195 | 196 | 197 | //setupAction.ResolveConflictingActions(apiDescriptions => 198 | //{ 199 | // return apiDescriptions.First(); 200 | //}); 201 | 202 | setupAction.OperationFilter(); 203 | setupAction.OperationFilter(); 204 | 205 | var xmlCommentsFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; 206 | var xmlCommentsFullPath = Path.Combine( 207 | AppContext.BaseDirectory, xmlCommentsFile); 208 | 209 | setupAction.IncludeXmlComments(xmlCommentsFullPath); 210 | }); 211 | 212 | // configure basic authentication 213 | builder.Services.AddAuthentication("Basic") 214 | .AddScheme("Basic", null); 216 | 217 | var app = builder.Build(); 218 | 219 | // Configure the HTTP request pipeline. 220 | 221 | app.UseStaticFiles(); 222 | 223 | app.UseSwagger(); 224 | 225 | app.UseSwaggerUI(setupAction => 226 | { 227 | // build a swagger endpoint for each discovered API version 228 | foreach (var description in 229 | apiVersionDescriptionProvider.ApiVersionDescriptions) 230 | { 231 | setupAction.SwaggerEndpoint( 232 | $"/swagger/LibraryOpenAPISpecification{description.GroupName}/swagger.json", 233 | description.GroupName.ToUpperInvariant()); 234 | } 235 | 236 | setupAction.DefaultModelExpandDepth(2); 237 | setupAction.DefaultModelRendering( 238 | Swashbuckle.AspNetCore.SwaggerUI.ModelRendering.Model); 239 | setupAction.DocExpansion( 240 | Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None); 241 | setupAction.EnableDeepLinking(); 242 | setupAction.DisplayOperationId(); 243 | 244 | setupAction.InjectStylesheet("/assets/custom-ui.css"); 245 | 246 | setupAction.IndexStream = () => typeof(Program) 247 | .Assembly.GetManifestResourceStream( 248 | "Library.API.EmbeddedAssets.index.html"); 249 | 250 | //setupAction.SwaggerEndpoint("/swagger/LibraryOpenAPISpecification/swagger.json", 251 | // "Library API"); 252 | //setupAction.SwaggerEndpoint( 253 | // "/swagger/LibraryOpenAPISpecificationAuthors/swagger.json", 254 | // "Library API (Authors)"); 255 | //setupAction.SwaggerEndpoint( 256 | // "/swagger/LibraryOpenAPISpecificationBooks/swagger.json", 257 | // "Library API (Books)"); 258 | setupAction.RoutePrefix = string.Empty; 259 | }); 260 | 261 | app.UseHttpsRedirection(); 262 | 263 | app.UseAuthentication(); 264 | 265 | app.UseAuthorization(); 266 | 267 | app.MapControllers(); 268 | 269 | app.Run(); 270 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:55069", 8 | "sslPort": 44382 9 | } 10 | }, 11 | "profiles": { 12 | "Library.API": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "applicationUrl": "http://localhost:5251", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "IIS Express": { 22 | "commandName": "IISExpress", 23 | "launchBrowser": true, 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Services/AuthorRepository.cs: -------------------------------------------------------------------------------- 1 | using Library.API.Contexts; 2 | using Library.API.Entities; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace Library.API.Services 6 | { 7 | public class AuthorRepository : IAuthorRepository 8 | { 9 | private readonly LibraryContext _context; 10 | 11 | public AuthorRepository(LibraryContext context) 12 | { 13 | _context = context; 14 | } 15 | 16 | public async Task AuthorExistsAsync(Guid authorId) 17 | { 18 | return await _context.Authors.AnyAsync(a => a.Id == authorId); 19 | } 20 | 21 | public async Task> GetAuthorsAsync() 22 | { 23 | return await _context.Authors.ToListAsync(); 24 | } 25 | 26 | public async Task GetAuthorAsync(Guid authorId) 27 | { 28 | if (authorId == Guid.Empty) 29 | { 30 | throw new ArgumentException("Argument can not be null or empty.", 31 | nameof(authorId)); 32 | } 33 | 34 | return await _context.Authors 35 | .FirstOrDefaultAsync(a => a.Id == authorId); 36 | } 37 | 38 | public void UpdateAuthor(Author author) 39 | { 40 | // no code in this implementation 41 | } 42 | 43 | public async Task SaveChangesAsync() 44 | { 45 | // return true if 1 or more entities were changed 46 | return (await _context.SaveChangesAsync() > 0); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Services/BookRepository.cs: -------------------------------------------------------------------------------- 1 | using Library.API.Contexts; 2 | using Library.API.Entities; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace Library.API.Services 6 | { 7 | public class BookRepository : IBookRepository 8 | { 9 | private readonly LibraryContext _context; 10 | 11 | public BookRepository(LibraryContext context) 12 | { 13 | _context = context ?? throw new ArgumentNullException(nameof(context)); 14 | } 15 | 16 | public async Task> GetBooksAsync(Guid authorId) 17 | { 18 | if (authorId == Guid.Empty) 19 | { 20 | throw new ArgumentException("Argument can not be null or empty.", 21 | nameof(authorId)); 22 | } 23 | 24 | return await _context.Books 25 | .Include(b => b.Author) 26 | .Where(b => b.AuthorId == authorId) 27 | .ToListAsync(); 28 | } 29 | 30 | public async Task GetBookAsync(Guid authorId, Guid bookId) 31 | { 32 | if (authorId == Guid.Empty) 33 | { 34 | throw new ArgumentException("Argument can not be null or empty.", 35 | nameof(authorId)); 36 | } 37 | 38 | if (bookId == Guid.Empty) 39 | { 40 | throw new ArgumentException("Argument can not be null or empty.", 41 | nameof(bookId)); 42 | } 43 | 44 | return await _context.Books 45 | .Include(b => b.Author) 46 | .Where(b => b.AuthorId == authorId && b.Id == bookId) 47 | .FirstOrDefaultAsync(); 48 | } 49 | 50 | public void AddBook(Book bookToAdd) 51 | { 52 | if (bookToAdd == null) 53 | { 54 | throw new ArgumentNullException(nameof(bookToAdd)); 55 | } 56 | 57 | _context.Add(bookToAdd); 58 | } 59 | 60 | public async Task SaveChangesAsync() 61 | { 62 | // return true if 1 or more entities were changed 63 | return (await _context.SaveChangesAsync() > 0); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Services/IAuthorRepository.cs: -------------------------------------------------------------------------------- 1 | using Library.API.Entities; 2 | 3 | namespace Library.API.Services 4 | { 5 | public interface IAuthorRepository 6 | { 7 | Task AuthorExistsAsync(Guid authorId); 8 | 9 | Task> GetAuthorsAsync(); 10 | 11 | Task GetAuthorAsync(Guid authorId); 12 | 13 | void UpdateAuthor(Author author); 14 | 15 | Task SaveChangesAsync(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Finished sample/Library.API/Services/IBookRepository.cs: -------------------------------------------------------------------------------- 1 | using Library.API.Entities; 2 | 3 | namespace Library.API.Services 4 | { 5 | public interface IBookRepository 6 | { 7 | Task> GetBooksAsync(Guid authorId); 8 | 9 | Task GetBookAsync(Guid authorId, Guid bookId); 10 | 11 | void AddBook(Book bookToAdd); 12 | 13 | Task SaveChangesAsync(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Finished sample/Library.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Finished sample/Library.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "ConnectionStrings": { 10 | "LibraryDBConnectionString": "Data Source=LibraryAPI.db" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Finished sample/Library.API/wwwroot/assets/custom-ui.css: -------------------------------------------------------------------------------- 1 | #swagger-ui { 2 | max-width: 800px; 3 | margin: auto; 4 | } 5 | 6 | .opblock-summary-method { 7 | border-color: #000000 !important; 8 | background: #000000 !important; 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Documenting an ASP.NET Core 6 Web API using Swagger 2 | Starter files & fully functioning finished sample code for my Documenting an ASP.NET Core 6 Web API using Swagger course. 3 | -------------------------------------------------------------------------------- /Starter files/Library.API.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31919.166 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Library.API", "Library.API\Library.API.csproj", "{981AFB52-5D9D-44F3-A1D9-70A61E09526F}" 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 | {981AFB52-5D9D-44F3-A1D9-70A61E09526F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {981AFB52-5D9D-44F3-A1D9-70A61E09526F}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {981AFB52-5D9D-44F3-A1D9-70A61E09526F}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {981AFB52-5D9D-44F3-A1D9-70A61E09526F}.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 = {8BE4CD99-BAD3-48B4-B950-901EE4C81D70} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Starter files/Library.API/Attributes/RequestHeaderMatchesMediaTypeAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.ActionConstraints; 2 | using Microsoft.AspNetCore.Mvc.Formatters; 3 | using Microsoft.Net.Http.Headers; 4 | 5 | namespace Library.API.Attributes 6 | { 7 | [AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)] 8 | public class RequestHeaderMatchesMediaTypeAttribute : Attribute, IActionConstraint 9 | { 10 | private readonly MediaTypeCollection _mediaTypes = new (); 11 | private readonly string _requestHeaderToMatch; 12 | 13 | public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch, 14 | string mediaType, params string[] otherMediaTypes) 15 | { 16 | _requestHeaderToMatch = requestHeaderToMatch 17 | ?? throw new ArgumentNullException(nameof(requestHeaderToMatch)); 18 | 19 | // check if the inputted media types are valid media types 20 | // and add them to the _mediaTypes collection 21 | 22 | if (MediaTypeHeaderValue.TryParse(mediaType, 23 | out MediaTypeHeaderValue? parsedMediaType)) 24 | { 25 | _mediaTypes.Add(parsedMediaType); 26 | } 27 | else 28 | { 29 | throw new ArgumentException("Argument can not be null or empty.", 30 | nameof(mediaType)); 31 | } 32 | 33 | foreach (var otherMediaType in otherMediaTypes) 34 | { 35 | if (MediaTypeHeaderValue.TryParse(otherMediaType, 36 | out MediaTypeHeaderValue? parsedOtherMediaType)) 37 | { 38 | _mediaTypes.Add(parsedOtherMediaType); 39 | } 40 | else 41 | { 42 | throw new ArgumentException("Argument can not be null or empty.", 43 | nameof(otherMediaTypes)); 44 | } 45 | } 46 | 47 | } 48 | 49 | public int Order { get; } 50 | 51 | public bool Accept(ActionConstraintContext context) 52 | { 53 | var requestHeaders = context.RouteContext.HttpContext.Request.Headers; 54 | if (!requestHeaders.ContainsKey(_requestHeaderToMatch)) 55 | { 56 | return false; 57 | } 58 | 59 | var parsedRequestMediaType = new MediaType(requestHeaders[_requestHeaderToMatch]); 60 | 61 | // if one of the media types matches, return true 62 | foreach (var mediaType in _mediaTypes) 63 | { 64 | var parsedMediaType = new MediaType(mediaType); 65 | if (parsedRequestMediaType.Equals(parsedMediaType)) 66 | { 67 | return true; 68 | } 69 | } 70 | return false; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Starter files/Library.API/Contexts/LibraryContext.cs: -------------------------------------------------------------------------------- 1 | using Library.API.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using System; 4 | 5 | namespace Library.API.Contexts 6 | { 7 | public class LibraryContext : DbContext 8 | { 9 | public DbSet Books { get; set; } = null!; 10 | 11 | public DbSet Authors { get; set; } = null!; 12 | 13 | public LibraryContext(DbContextOptions options) 14 | : base(options) 15 | { 16 | } 17 | 18 | protected override void OnModelCreating(ModelBuilder modelBuilder) 19 | { 20 | // seed the database with dummy data 21 | modelBuilder.Entity().HasData( 22 | new Author() 23 | { 24 | Id = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 25 | FirstName = "George", 26 | LastName = "RR Martin" 27 | }, 28 | new Author() 29 | { 30 | Id = Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 31 | FirstName = "Stephen", 32 | LastName = "Fry" 33 | }, 34 | new Author() 35 | { 36 | Id = Guid.Parse("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"), 37 | FirstName = "James", 38 | LastName = "Elroy" 39 | }, 40 | new Author() 41 | { 42 | Id = Guid.Parse("2902b665-1190-4c70-9915-b9c2d7680450"), 43 | FirstName = "Douglas", 44 | LastName = "Adams" 45 | } 46 | ); 47 | 48 | // seed the database with dummy data 49 | modelBuilder.Entity().HasData( 50 | new Book("A Dance with Dragons") 51 | { 52 | Id = Guid.Parse("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 53 | AuthorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 54 | Description = "A Dance with Dragons is the fifth of seven planned novels in the epic fantasy series A Song of Ice and Fire." 55 | }, 56 | new Book("A Game of Thrones") 57 | { 58 | Id = Guid.Parse("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 59 | AuthorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 60 | Description = "A Game of Thrones is the first novel in A Song of Ice and Fire, a series of fantasy novels by American author George R. R. ... In the novel, recounting events from various points of view, Martin introduces the plot-lines of the noble houses of Westeros, the Wall, and the Targaryens." 61 | }, 62 | new Book("Mythos") 63 | { 64 | Id = Guid.Parse("d173e20d-159e-4127-9ce9-b0ac2564ad97"), 65 | AuthorId = Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 66 | Description = "The Greek myths are amongst the best stories ever told, passed down through millennia and inspiring writers and artists as varied as Shakespeare, Michelangelo, James Joyce and Walt Disney. They are embedded deeply in the traditions, tales and cultural DNA of the West.You'll fall in love with Zeus, marvel at the birth of Athena, wince at Cronus and Gaia's revenge on Ouranos, weep with King Midas and hunt with the beautiful and ferocious Artemis. Spellbinding, informative and moving, Stephen Fry's Mythos perfectly captures these stories for the modern age - in all their rich and deeply human relevance." 67 | }, 68 | new Book("American Tabloid") 69 | { 70 | Id = Guid.Parse("493c3228-3444-4a49-9cc0-e8532edc59b2"), 71 | AuthorId = Guid.Parse("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"), 72 | Description = "American Tabloid is a 1995 novel by James Ellroy that chronicles the events surrounding three rogue American law enforcement officers from November 22, 1958 through November 22, 1963. Each becomes entangled in a web of interconnecting associations between the FBI, the CIA, and the mafia, which eventually leads to their collective involvement in the John F. Kennedy assassination." 73 | }, 74 | new Book("The Hitchhiker's Guide to the Galaxy") 75 | { 76 | Id = Guid.Parse("40ff5488-fdab-45b5-bc3a-14302d59869a"), 77 | AuthorId = Guid.Parse("2902b665-1190-4c70-9915-b9c2d7680450"), 78 | Description = "In The Hitchhiker's Guide to the Galaxy, the characters visit the legendary planet Magrathea, home to the now-collapsed planet-building industry, and meet Slartibartfast, a planetary coastline designer who was responsible for the fjords of Norway. Through archival recordings, he relates the story of a race of hyper-intelligent pan-dimensional beings who built a computer named Deep Thought to calculate the Answer to the Ultimate Question of Life, the Universe, and Everything." 79 | } 80 | ); 81 | 82 | base.OnModelCreating(modelBuilder); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Starter files/Library.API/Controllers/AuthorsController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Library.API.Models; 3 | using Library.API.Services; 4 | using Microsoft.AspNetCore.JsonPatch; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace Library.API.Controllers 8 | { 9 | [Route("api/authors")] 10 | [ApiController] 11 | public class AuthorsController : ControllerBase 12 | { 13 | private readonly IAuthorRepository _authorsRepository; 14 | private readonly IMapper _mapper; 15 | 16 | public AuthorsController( 17 | IAuthorRepository authorsRepository, 18 | IMapper mapper) 19 | { 20 | _authorsRepository = authorsRepository 21 | ?? throw new ArgumentNullException(nameof(authorsRepository)); 22 | _mapper = mapper 23 | ?? throw new ArgumentNullException(nameof(mapper)); 24 | } 25 | 26 | [HttpGet] 27 | public async Task>> GetAuthors() 28 | { 29 | var authorsFromRepo = await _authorsRepository.GetAuthorsAsync(); 30 | return Ok(_mapper.Map>(authorsFromRepo)); 31 | } 32 | 33 | [HttpGet("{authorId}")] 34 | public async Task> GetAuthor( 35 | Guid authorId) 36 | { 37 | var authorFromRepo = await _authorsRepository.GetAuthorAsync(authorId); 38 | if (authorFromRepo == null) 39 | { 40 | return NotFound(); 41 | } 42 | 43 | return Ok(_mapper.Map(authorFromRepo)); 44 | } 45 | 46 | [HttpPut("{authorId}")] 47 | public async Task> UpdateAuthor( 48 | Guid authorId, 49 | AuthorForUpdate authorForUpdate) 50 | { 51 | var authorFromRepo = await _authorsRepository.GetAuthorAsync(authorId); 52 | if (authorFromRepo == null) 53 | { 54 | return NotFound(); 55 | } 56 | 57 | _mapper.Map(authorForUpdate, authorFromRepo); 58 | 59 | // update & save 60 | _authorsRepository.UpdateAuthor(authorFromRepo); 61 | await _authorsRepository.SaveChangesAsync(); 62 | 63 | // return the author 64 | return Ok(_mapper.Map(authorFromRepo)); 65 | } 66 | 67 | [HttpPatch("{authorId}")] 68 | public async Task> UpdateAuthor( 69 | Guid authorId, 70 | JsonPatchDocument patchDocument) 71 | { 72 | var authorFromRepo = await _authorsRepository.GetAuthorAsync(authorId); 73 | if (authorFromRepo == null) 74 | { 75 | return NotFound(); 76 | } 77 | 78 | // map to DTO to apply the patch to 79 | var author = _mapper.Map(authorFromRepo); 80 | patchDocument.ApplyTo(author, ModelState); 81 | 82 | // if there are errors when applying the patch the patch doc 83 | // was badly formed These aren't caught via the ApiController 84 | // validation, so we must manually check the modelstate and 85 | // potentially return these errors. 86 | if (!ModelState.IsValid) 87 | { 88 | return new UnprocessableEntityObjectResult(ModelState); 89 | } 90 | 91 | // map the applied changes on the DTO back into the entity 92 | _mapper.Map(author, authorFromRepo); 93 | 94 | // update & save 95 | _authorsRepository.UpdateAuthor(authorFromRepo); 96 | await _authorsRepository.SaveChangesAsync(); 97 | 98 | // return the author 99 | return Ok(_mapper.Map(authorFromRepo)); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Starter files/Library.API/Controllers/BooksController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Library.API.Models; 3 | using Library.API.Services; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace Library.API.Controllers 7 | { 8 | [Route("api/authors/{authorId}/books")] 9 | [ApiController] 10 | public class BooksController : ControllerBase 11 | { 12 | private readonly IBookRepository _bookRepository; 13 | private readonly IAuthorRepository _authorRepository; 14 | private readonly IMapper _mapper; 15 | 16 | public BooksController( 17 | IBookRepository bookRepository, 18 | IAuthorRepository authorRepository, 19 | IMapper mapper) 20 | { 21 | _bookRepository = bookRepository 22 | ?? throw new ArgumentNullException(nameof(bookRepository)); 23 | _authorRepository = authorRepository 24 | ?? throw new ArgumentNullException(nameof(authorRepository)); 25 | _mapper = mapper 26 | ?? throw new ArgumentNullException(nameof(mapper)); 27 | } 28 | 29 | [HttpGet()] 30 | public async Task>> GetBooks( 31 | Guid authorId) 32 | { 33 | if (!await _authorRepository.AuthorExistsAsync(authorId)) 34 | { 35 | return NotFound(); 36 | } 37 | 38 | var booksFromRepo = await _bookRepository.GetBooksAsync(authorId); 39 | return Ok(_mapper.Map>(booksFromRepo)); 40 | } 41 | 42 | [HttpGet("{bookId}")] 43 | public async Task> GetBook( 44 | Guid authorId, 45 | Guid bookId) 46 | { 47 | if (!await _authorRepository.AuthorExistsAsync(authorId)) 48 | { 49 | return NotFound(); 50 | } 51 | 52 | var bookFromRepo = await _bookRepository.GetBookAsync(authorId, bookId); 53 | if (bookFromRepo == null) 54 | { 55 | return NotFound(); 56 | } 57 | 58 | return Ok(_mapper.Map(bookFromRepo)); 59 | } 60 | 61 | 62 | [HttpPost()] 63 | public async Task> CreateBook( 64 | Guid authorId, 65 | BookForCreation bookForCreation) 66 | { 67 | if (!await _authorRepository.AuthorExistsAsync(authorId)) 68 | { 69 | return NotFound(); 70 | } 71 | 72 | var bookToAdd = _mapper.Map(bookForCreation); 73 | _bookRepository.AddBook(bookToAdd); 74 | await _bookRepository.SaveChangesAsync(); 75 | 76 | return CreatedAtRoute( 77 | "GetBook", 78 | new { authorId, bookId = bookToAdd.Id }, 79 | _mapper.Map(bookToAdd)); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Starter files/Library.API/Entities/Author.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | namespace Library.API.Entities 7 | { 8 | 9 | [Table("Authors")] 10 | public class Author 11 | { 12 | [Key] 13 | public Guid Id { get; set; } 14 | 15 | [Required] 16 | [MaxLength(150)] 17 | public string FirstName { get; set; } = string.Empty; 18 | 19 | [Required] 20 | [MaxLength(150)] 21 | public string LastName { get; set; } = string.Empty; 22 | 23 | public ICollection Books { get; set; } = new List(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Starter files/Library.API/Entities/Book.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace Library.API.Entities 5 | { 6 | [Table("Books")] 7 | public class Book 8 | { 9 | [Key] 10 | public Guid Id { get; set; } 11 | 12 | [Required] 13 | [MaxLength(150)] 14 | public string Title { get; set; } = string.Empty; 15 | 16 | [MaxLength(2500)] 17 | public string? Description { get; set; } 18 | 19 | public int? AmountOfPages { get; set; } 20 | 21 | public Guid AuthorId { get; set; } 22 | public Author? Author { get; set; } 23 | 24 | public Book(string title) 25 | { 26 | Title = title; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Starter files/Library.API/IListExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Library.API 5 | { 6 | public static class IListExtensions 7 | { 8 | public static void AddRange(this IList list, IEnumerable items) 9 | { 10 | if (list == null) 11 | { 12 | throw new ArgumentNullException("list"); 13 | } 14 | 15 | if (items == null) 16 | { 17 | throw new ArgumentNullException("items"); 18 | } 19 | 20 | if (list is List) 21 | { 22 | ((List)list).AddRange(items); 23 | } 24 | else 25 | { 26 | foreach (var item in items) 27 | { 28 | list.Add(item); 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Starter files/Library.API/Library.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Starter files/Library.API/LibraryAPI.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinDockx/DocumentingAspNetCore6API/346a2cd0a181da1091a1c0f60db836bde56a488c/Starter files/Library.API/LibraryAPI.db -------------------------------------------------------------------------------- /Starter files/Library.API/LibraryAPI.db-shm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinDockx/DocumentingAspNetCore6API/346a2cd0a181da1091a1c0f60db836bde56a488c/Starter files/Library.API/LibraryAPI.db-shm -------------------------------------------------------------------------------- /Starter files/Library.API/LibraryAPI.db-wal: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinDockx/DocumentingAspNetCore6API/346a2cd0a181da1091a1c0f60db836bde56a488c/Starter files/Library.API/LibraryAPI.db-wal -------------------------------------------------------------------------------- /Starter files/Library.API/Migrations/InitialMigration.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Library.API.Contexts; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | #nullable disable 10 | 11 | namespace Library.API.Migrations 12 | { 13 | [DbContext(typeof(LibraryContext))] 14 | [Migration("20220316134041_InitialMigration")] 15 | partial class InitialMigration 16 | { 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder.HasAnnotation("ProductVersion", "6.0.3"); 21 | 22 | modelBuilder.Entity("Library.API.Entities.Author", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd() 26 | .HasColumnType("TEXT"); 27 | 28 | b.Property("FirstName") 29 | .IsRequired() 30 | .HasMaxLength(150) 31 | .HasColumnType("TEXT"); 32 | 33 | b.Property("LastName") 34 | .IsRequired() 35 | .HasMaxLength(150) 36 | .HasColumnType("TEXT"); 37 | 38 | b.HasKey("Id"); 39 | 40 | b.ToTable("Authors"); 41 | 42 | b.HasData( 43 | new 44 | { 45 | Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 46 | FirstName = "George", 47 | LastName = "RR Martin" 48 | }, 49 | new 50 | { 51 | Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 52 | FirstName = "Stephen", 53 | LastName = "Fry" 54 | }, 55 | new 56 | { 57 | Id = new Guid("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"), 58 | FirstName = "James", 59 | LastName = "Elroy" 60 | }, 61 | new 62 | { 63 | Id = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 64 | FirstName = "Douglas", 65 | LastName = "Adams" 66 | }); 67 | }); 68 | 69 | modelBuilder.Entity("Library.API.Entities.Book", b => 70 | { 71 | b.Property("Id") 72 | .ValueGeneratedOnAdd() 73 | .HasColumnType("TEXT"); 74 | 75 | b.Property("AmountOfPages") 76 | .HasColumnType("INTEGER"); 77 | 78 | b.Property("AuthorId") 79 | .HasColumnType("TEXT"); 80 | 81 | b.Property("Description") 82 | .HasMaxLength(2500) 83 | .HasColumnType("TEXT"); 84 | 85 | b.Property("Title") 86 | .IsRequired() 87 | .HasMaxLength(150) 88 | .HasColumnType("TEXT"); 89 | 90 | b.HasKey("Id"); 91 | 92 | b.HasIndex("AuthorId"); 93 | 94 | b.ToTable("Books"); 95 | 96 | b.HasData( 97 | new 98 | { 99 | Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 100 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 101 | Description = "A Dance with Dragons is the fifth of seven planned novels in the epic fantasy series A Song of Ice and Fire.", 102 | Title = "A Dance with Dragons" 103 | }, 104 | new 105 | { 106 | Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 107 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 108 | Description = "A Game of Thrones is the first novel in A Song of Ice and Fire, a series of fantasy novels by American author George R. R. ... In the novel, recounting events from various points of view, Martin introduces the plot-lines of the noble houses of Westeros, the Wall, and the Targaryens.", 109 | Title = "A Game of Thrones" 110 | }, 111 | new 112 | { 113 | Id = new Guid("d173e20d-159e-4127-9ce9-b0ac2564ad97"), 114 | AuthorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 115 | Description = "The Greek myths are amongst the best stories ever told, passed down through millennia and inspiring writers and artists as varied as Shakespeare, Michelangelo, James Joyce and Walt Disney. They are embedded deeply in the traditions, tales and cultural DNA of the West.You'll fall in love with Zeus, marvel at the birth of Athena, wince at Cronus and Gaia's revenge on Ouranos, weep with King Midas and hunt with the beautiful and ferocious Artemis. Spellbinding, informative and moving, Stephen Fry's Mythos perfectly captures these stories for the modern age - in all their rich and deeply human relevance.", 116 | Title = "Mythos" 117 | }, 118 | new 119 | { 120 | Id = new Guid("493c3228-3444-4a49-9cc0-e8532edc59b2"), 121 | AuthorId = new Guid("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"), 122 | Description = "American Tabloid is a 1995 novel by James Ellroy that chronicles the events surrounding three rogue American law enforcement officers from November 22, 1958 through November 22, 1963. Each becomes entangled in a web of interconnecting associations between the FBI, the CIA, and the mafia, which eventually leads to their collective involvement in the John F. Kennedy assassination.", 123 | Title = "American Tabloid" 124 | }, 125 | new 126 | { 127 | Id = new Guid("40ff5488-fdab-45b5-bc3a-14302d59869a"), 128 | AuthorId = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 129 | Description = "In The Hitchhiker's Guide to the Galaxy, the characters visit the legendary planet Magrathea, home to the now-collapsed planet-building industry, and meet Slartibartfast, a planetary coastline designer who was responsible for the fjords of Norway. Through archival recordings, he relates the story of a race of hyper-intelligent pan-dimensional beings who built a computer named Deep Thought to calculate the Answer to the Ultimate Question of Life, the Universe, and Everything.", 130 | Title = "The Hitchhiker's Guide to the Galaxy" 131 | }); 132 | }); 133 | 134 | modelBuilder.Entity("Library.API.Entities.Book", b => 135 | { 136 | b.HasOne("Library.API.Entities.Author", "Author") 137 | .WithMany("Books") 138 | .HasForeignKey("AuthorId") 139 | .OnDelete(DeleteBehavior.Cascade) 140 | .IsRequired(); 141 | 142 | b.Navigation("Author"); 143 | }); 144 | 145 | modelBuilder.Entity("Library.API.Entities.Author", b => 146 | { 147 | b.Navigation("Books"); 148 | }); 149 | #pragma warning restore 612, 618 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Starter files/Library.API/Migrations/InitialMigration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace Library.API.Migrations 7 | { 8 | public partial class InitialMigration : Migration 9 | { 10 | protected override void Up(MigrationBuilder migrationBuilder) 11 | { 12 | migrationBuilder.CreateTable( 13 | name: "Authors", 14 | columns: table => new 15 | { 16 | Id = table.Column(type: "TEXT", nullable: false), 17 | FirstName = table.Column(type: "TEXT", maxLength: 150, nullable: false), 18 | LastName = table.Column(type: "TEXT", maxLength: 150, nullable: false) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Authors", x => x.Id); 23 | }); 24 | 25 | migrationBuilder.CreateTable( 26 | name: "Books", 27 | columns: table => new 28 | { 29 | Id = table.Column(type: "TEXT", nullable: false), 30 | Title = table.Column(type: "TEXT", maxLength: 150, nullable: false), 31 | Description = table.Column(type: "TEXT", maxLength: 2500, nullable: true), 32 | AmountOfPages = table.Column(type: "INTEGER", nullable: true), 33 | AuthorId = table.Column(type: "TEXT", nullable: false) 34 | }, 35 | constraints: table => 36 | { 37 | table.PrimaryKey("PK_Books", x => x.Id); 38 | table.ForeignKey( 39 | name: "FK_Books_Authors_AuthorId", 40 | column: x => x.AuthorId, 41 | principalTable: "Authors", 42 | principalColumn: "Id", 43 | onDelete: ReferentialAction.Cascade); 44 | }); 45 | 46 | migrationBuilder.InsertData( 47 | table: "Authors", 48 | columns: new[] { "Id", "FirstName", "LastName" }, 49 | values: new object[] { new Guid("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"), "James", "Elroy" }); 50 | 51 | migrationBuilder.InsertData( 52 | table: "Authors", 53 | columns: new[] { "Id", "FirstName", "LastName" }, 54 | values: new object[] { new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), "Douglas", "Adams" }); 55 | 56 | migrationBuilder.InsertData( 57 | table: "Authors", 58 | columns: new[] { "Id", "FirstName", "LastName" }, 59 | values: new object[] { new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), "George", "RR Martin" }); 60 | 61 | migrationBuilder.InsertData( 62 | table: "Authors", 63 | columns: new[] { "Id", "FirstName", "LastName" }, 64 | values: new object[] { new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), "Stephen", "Fry" }); 65 | 66 | migrationBuilder.InsertData( 67 | table: "Books", 68 | columns: new[] { "Id", "AmountOfPages", "AuthorId", "Description", "Title" }, 69 | values: new object[] { new Guid("40ff5488-fdab-45b5-bc3a-14302d59869a"), null, new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), "In The Hitchhiker's Guide to the Galaxy, the characters visit the legendary planet Magrathea, home to the now-collapsed planet-building industry, and meet Slartibartfast, a planetary coastline designer who was responsible for the fjords of Norway. Through archival recordings, he relates the story of a race of hyper-intelligent pan-dimensional beings who built a computer named Deep Thought to calculate the Answer to the Ultimate Question of Life, the Universe, and Everything.", "The Hitchhiker's Guide to the Galaxy" }); 70 | 71 | migrationBuilder.InsertData( 72 | table: "Books", 73 | columns: new[] { "Id", "AmountOfPages", "AuthorId", "Description", "Title" }, 74 | values: new object[] { new Guid("493c3228-3444-4a49-9cc0-e8532edc59b2"), null, new Guid("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"), "American Tabloid is a 1995 novel by James Ellroy that chronicles the events surrounding three rogue American law enforcement officers from November 22, 1958 through November 22, 1963. Each becomes entangled in a web of interconnecting associations between the FBI, the CIA, and the mafia, which eventually leads to their collective involvement in the John F. Kennedy assassination.", "American Tabloid" }); 75 | 76 | migrationBuilder.InsertData( 77 | table: "Books", 78 | columns: new[] { "Id", "AmountOfPages", "AuthorId", "Description", "Title" }, 79 | values: new object[] { new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), null, new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), "A Dance with Dragons is the fifth of seven planned novels in the epic fantasy series A Song of Ice and Fire.", "A Dance with Dragons" }); 80 | 81 | migrationBuilder.InsertData( 82 | table: "Books", 83 | columns: new[] { "Id", "AmountOfPages", "AuthorId", "Description", "Title" }, 84 | values: new object[] { new Guid("d173e20d-159e-4127-9ce9-b0ac2564ad97"), null, new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), "The Greek myths are amongst the best stories ever told, passed down through millennia and inspiring writers and artists as varied as Shakespeare, Michelangelo, James Joyce and Walt Disney. They are embedded deeply in the traditions, tales and cultural DNA of the West.You'll fall in love with Zeus, marvel at the birth of Athena, wince at Cronus and Gaia's revenge on Ouranos, weep with King Midas and hunt with the beautiful and ferocious Artemis. Spellbinding, informative and moving, Stephen Fry's Mythos perfectly captures these stories for the modern age - in all their rich and deeply human relevance.", "Mythos" }); 85 | 86 | migrationBuilder.InsertData( 87 | table: "Books", 88 | columns: new[] { "Id", "AmountOfPages", "AuthorId", "Description", "Title" }, 89 | values: new object[] { new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), null, new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), "A Game of Thrones is the first novel in A Song of Ice and Fire, a series of fantasy novels by American author George R. R. ... In the novel, recounting events from various points of view, Martin introduces the plot-lines of the noble houses of Westeros, the Wall, and the Targaryens.", "A Game of Thrones" }); 90 | 91 | migrationBuilder.CreateIndex( 92 | name: "IX_Books_AuthorId", 93 | table: "Books", 94 | column: "AuthorId"); 95 | } 96 | 97 | protected override void Down(MigrationBuilder migrationBuilder) 98 | { 99 | migrationBuilder.DropTable( 100 | name: "Books"); 101 | 102 | migrationBuilder.DropTable( 103 | name: "Authors"); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Starter files/Library.API/Migrations/LibraryContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Library.API.Contexts; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | 8 | #nullable disable 9 | 10 | namespace Library.API.Migrations 11 | { 12 | [DbContext(typeof(LibraryContext))] 13 | partial class LibraryContextModelSnapshot : ModelSnapshot 14 | { 15 | protected override void BuildModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder.HasAnnotation("ProductVersion", "6.0.3"); 19 | 20 | modelBuilder.Entity("Library.API.Entities.Author", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd() 24 | .HasColumnType("TEXT"); 25 | 26 | b.Property("FirstName") 27 | .IsRequired() 28 | .HasMaxLength(150) 29 | .HasColumnType("TEXT"); 30 | 31 | b.Property("LastName") 32 | .IsRequired() 33 | .HasMaxLength(150) 34 | .HasColumnType("TEXT"); 35 | 36 | b.HasKey("Id"); 37 | 38 | b.ToTable("Authors"); 39 | 40 | b.HasData( 41 | new 42 | { 43 | Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 44 | FirstName = "George", 45 | LastName = "RR Martin" 46 | }, 47 | new 48 | { 49 | Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 50 | FirstName = "Stephen", 51 | LastName = "Fry" 52 | }, 53 | new 54 | { 55 | Id = new Guid("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"), 56 | FirstName = "James", 57 | LastName = "Elroy" 58 | }, 59 | new 60 | { 61 | Id = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 62 | FirstName = "Douglas", 63 | LastName = "Adams" 64 | }); 65 | }); 66 | 67 | modelBuilder.Entity("Library.API.Entities.Book", b => 68 | { 69 | b.Property("Id") 70 | .ValueGeneratedOnAdd() 71 | .HasColumnType("TEXT"); 72 | 73 | b.Property("AmountOfPages") 74 | .HasColumnType("INTEGER"); 75 | 76 | b.Property("AuthorId") 77 | .HasColumnType("TEXT"); 78 | 79 | b.Property("Description") 80 | .HasMaxLength(2500) 81 | .HasColumnType("TEXT"); 82 | 83 | b.Property("Title") 84 | .IsRequired() 85 | .HasMaxLength(150) 86 | .HasColumnType("TEXT"); 87 | 88 | b.HasKey("Id"); 89 | 90 | b.HasIndex("AuthorId"); 91 | 92 | b.ToTable("Books"); 93 | 94 | b.HasData( 95 | new 96 | { 97 | Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 98 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 99 | Description = "A Dance with Dragons is the fifth of seven planned novels in the epic fantasy series A Song of Ice and Fire.", 100 | Title = "A Dance with Dragons" 101 | }, 102 | new 103 | { 104 | Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 105 | AuthorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 106 | Description = "A Game of Thrones is the first novel in A Song of Ice and Fire, a series of fantasy novels by American author George R. R. ... In the novel, recounting events from various points of view, Martin introduces the plot-lines of the noble houses of Westeros, the Wall, and the Targaryens.", 107 | Title = "A Game of Thrones" 108 | }, 109 | new 110 | { 111 | Id = new Guid("d173e20d-159e-4127-9ce9-b0ac2564ad97"), 112 | AuthorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 113 | Description = "The Greek myths are amongst the best stories ever told, passed down through millennia and inspiring writers and artists as varied as Shakespeare, Michelangelo, James Joyce and Walt Disney. They are embedded deeply in the traditions, tales and cultural DNA of the West.You'll fall in love with Zeus, marvel at the birth of Athena, wince at Cronus and Gaia's revenge on Ouranos, weep with King Midas and hunt with the beautiful and ferocious Artemis. Spellbinding, informative and moving, Stephen Fry's Mythos perfectly captures these stories for the modern age - in all their rich and deeply human relevance.", 114 | Title = "Mythos" 115 | }, 116 | new 117 | { 118 | Id = new Guid("493c3228-3444-4a49-9cc0-e8532edc59b2"), 119 | AuthorId = new Guid("24810dfc-2d94-4cc7-aab5-cdf98b83f0c9"), 120 | Description = "American Tabloid is a 1995 novel by James Ellroy that chronicles the events surrounding three rogue American law enforcement officers from November 22, 1958 through November 22, 1963. Each becomes entangled in a web of interconnecting associations between the FBI, the CIA, and the mafia, which eventually leads to their collective involvement in the John F. Kennedy assassination.", 121 | Title = "American Tabloid" 122 | }, 123 | new 124 | { 125 | Id = new Guid("40ff5488-fdab-45b5-bc3a-14302d59869a"), 126 | AuthorId = new Guid("2902b665-1190-4c70-9915-b9c2d7680450"), 127 | Description = "In The Hitchhiker's Guide to the Galaxy, the characters visit the legendary planet Magrathea, home to the now-collapsed planet-building industry, and meet Slartibartfast, a planetary coastline designer who was responsible for the fjords of Norway. Through archival recordings, he relates the story of a race of hyper-intelligent pan-dimensional beings who built a computer named Deep Thought to calculate the Answer to the Ultimate Question of Life, the Universe, and Everything.", 128 | Title = "The Hitchhiker's Guide to the Galaxy" 129 | }); 130 | }); 131 | 132 | modelBuilder.Entity("Library.API.Entities.Book", b => 133 | { 134 | b.HasOne("Library.API.Entities.Author", "Author") 135 | .WithMany("Books") 136 | .HasForeignKey("AuthorId") 137 | .OnDelete(DeleteBehavior.Cascade) 138 | .IsRequired(); 139 | 140 | b.Navigation("Author"); 141 | }); 142 | 143 | modelBuilder.Entity("Library.API.Entities.Author", b => 144 | { 145 | b.Navigation("Books"); 146 | }); 147 | #pragma warning restore 612, 618 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Starter files/Library.API/Models/Author.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Library.API.Models 4 | { 5 | 6 | public class Author 7 | { 8 | public Guid Id { get; set; } 9 | 10 | public string? FirstName { get; set; } 11 | 12 | public string? LastName { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Starter files/Library.API/Models/AuthorForUpdate.cs: -------------------------------------------------------------------------------- 1 | namespace Library.API.Models 2 | { 3 | public class AuthorForUpdate 4 | { 5 | public string? FirstName { get; set; } 6 | 7 | public string? LastName { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Starter files/Library.API/Models/Book.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Library.API.Models 4 | { 5 | public class Book 6 | { 7 | public Guid Id { get; set; } 8 | 9 | public string? AuthorFirstName { get; set; } 10 | 11 | public string? AuthorLastName { get; set; } 12 | 13 | public string? Title { get; set; } 14 | 15 | public string? Description { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Starter files/Library.API/Models/BookForCreation.cs: -------------------------------------------------------------------------------- 1 | namespace Library.API.Models 2 | { 3 | public class BookForCreation 4 | { 5 | public string? Title { get; set; } 6 | 7 | public string? Description { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Starter files/Library.API/Models/BookForCreationWithAmountOfPages.cs: -------------------------------------------------------------------------------- 1 | namespace Library.API.Models 2 | { 3 | public class BookForCreationWithAmountOfPages : BookForCreation 4 | { 5 | public int AmountOfPages { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Starter files/Library.API/Models/BookWithConcatenatedAuthorName.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Library.API.Models 4 | { 5 | public class BookWithConcatenatedAuthorName 6 | { 7 | public Guid Id { get; set; } 8 | 9 | public string? Author { get; set; } 10 | 11 | public string? Title { get; set; } 12 | 13 | public string? Description { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Starter files/Library.API/Profiles/AuthorProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace Library.API.Profiles 4 | { 5 | public class AuthorProfile : Profile 6 | { 7 | public AuthorProfile() 8 | { 9 | CreateMap(); 10 | 11 | CreateMap(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Starter files/Library.API/Profiles/BookProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace Library.API.Profiles 4 | { 5 | public class BookProfile : Profile 6 | { 7 | public BookProfile() 8 | { 9 | CreateMap() 10 | .ForMember(dest => dest.Author, opt => opt.MapFrom(src => 11 | src.Author == null ? "" : $"{src.Author.FirstName} {src.Author.LastName}") 12 | ); 13 | 14 | CreateMap() 15 | .ForMember(dest => dest.AuthorFirstName, opt => opt.MapFrom(src => 16 | src.Author == null ? "" : $"{src.Author.FirstName}")) 17 | .ForMember(dest => dest.AuthorLastName, opt => opt.MapFrom(src => 18 | src.Author == null ? "" : $"{src.Author.LastName}")); 19 | 20 | CreateMap(); 21 | 22 | CreateMap(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Starter files/Library.API/Program.cs: -------------------------------------------------------------------------------- 1 | using Library.API.Contexts; 2 | using Library.API.Services; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.Formatters; 5 | using Microsoft.EntityFrameworkCore; 6 | using Newtonsoft.Json.Serialization; 7 | 8 | var builder = WebApplication.CreateBuilder(args); 9 | 10 | // Add services to the container. 11 | builder.Services.AddControllers(configure => 12 | { 13 | configure.ReturnHttpNotAcceptable = true; 14 | }).AddNewtonsoftJson(setupAction => 15 | { 16 | setupAction.SerializerSettings.ContractResolver = 17 | new CamelCasePropertyNamesContractResolver(); 18 | }); 19 | 20 | // configure the NewtonsoftJsonOutputFormatter 21 | builder.Services.Configure(configureOptions => 22 | { 23 | var jsonOutputFormatter = configureOptions.OutputFormatters 24 | .OfType().FirstOrDefault(); 25 | 26 | if (jsonOutputFormatter != null) 27 | { 28 | // remove text/json as it isn't the approved media type 29 | // for working with JSON at API level 30 | if (jsonOutputFormatter.SupportedMediaTypes.Contains("text/json")) 31 | { 32 | jsonOutputFormatter.SupportedMediaTypes.Remove("text/json"); 33 | } 34 | } 35 | }); 36 | 37 | builder.Services.AddDbContext( 38 | dbContextOptions => dbContextOptions.UseSqlite( 39 | builder.Configuration["ConnectionStrings:LibraryDBConnectionString"])); 40 | 41 | builder.Services.AddScoped(); 42 | builder.Services.AddScoped(); 43 | 44 | builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); 45 | 46 | var app = builder.Build(); 47 | 48 | // Configure the HTTP request pipeline. 49 | 50 | app.UseHttpsRedirection(); 51 | 52 | app.UseAuthorization(); 53 | 54 | app.MapControllers(); 55 | 56 | app.Run(); 57 | -------------------------------------------------------------------------------- /Starter files/Library.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:55069", 8 | "sslPort": 44382 9 | } 10 | }, 11 | "profiles": { 12 | "Library.API": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "applicationUrl": "http://localhost:5251", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "IIS Express": { 22 | "commandName": "IISExpress", 23 | "launchBrowser": true, 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Starter files/Library.API/Services/AuthorRepository.cs: -------------------------------------------------------------------------------- 1 | using Library.API.Contexts; 2 | using Library.API.Entities; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace Library.API.Services 6 | { 7 | public class AuthorRepository : IAuthorRepository 8 | { 9 | private readonly LibraryContext _context; 10 | 11 | public AuthorRepository(LibraryContext context) 12 | { 13 | _context = context; 14 | } 15 | 16 | public async Task AuthorExistsAsync(Guid authorId) 17 | { 18 | return await _context.Authors.AnyAsync(a => a.Id == authorId); 19 | } 20 | 21 | public async Task> GetAuthorsAsync() 22 | { 23 | return await _context.Authors.ToListAsync(); 24 | } 25 | 26 | public async Task GetAuthorAsync(Guid authorId) 27 | { 28 | if (authorId == Guid.Empty) 29 | { 30 | throw new ArgumentException("Argument can not be null or empty.", 31 | nameof(authorId)); 32 | } 33 | 34 | return await _context.Authors 35 | .FirstOrDefaultAsync(a => a.Id == authorId); 36 | } 37 | 38 | public void UpdateAuthor(Author author) 39 | { 40 | // no code in this implementation 41 | } 42 | 43 | public async Task SaveChangesAsync() 44 | { 45 | // return true if 1 or more entities were changed 46 | return (await _context.SaveChangesAsync() > 0); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Starter files/Library.API/Services/BookRepository.cs: -------------------------------------------------------------------------------- 1 | using Library.API.Contexts; 2 | using Library.API.Entities; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace Library.API.Services 6 | { 7 | public class BookRepository : IBookRepository 8 | { 9 | private readonly LibraryContext _context; 10 | 11 | public BookRepository(LibraryContext context) 12 | { 13 | _context = context ?? throw new ArgumentNullException(nameof(context)); 14 | } 15 | 16 | public async Task> GetBooksAsync(Guid authorId) 17 | { 18 | if (authorId == Guid.Empty) 19 | { 20 | throw new ArgumentException("Argument can not be null or empty.", 21 | nameof(authorId)); 22 | } 23 | 24 | return await _context.Books 25 | .Include(b => b.Author) 26 | .Where(b => b.AuthorId == authorId) 27 | .ToListAsync(); 28 | } 29 | 30 | public async Task GetBookAsync(Guid authorId, Guid bookId) 31 | { 32 | if (authorId == Guid.Empty) 33 | { 34 | throw new ArgumentException("Argument can not be null or empty.", 35 | nameof(authorId)); 36 | } 37 | 38 | if (bookId == Guid.Empty) 39 | { 40 | throw new ArgumentException("Argument can not be null or empty.", 41 | nameof(bookId)); 42 | } 43 | 44 | return await _context.Books 45 | .Include(b => b.Author) 46 | .Where(b => b.AuthorId == authorId && b.Id == bookId) 47 | .FirstOrDefaultAsync(); 48 | } 49 | 50 | public void AddBook(Book bookToAdd) 51 | { 52 | if (bookToAdd == null) 53 | { 54 | throw new ArgumentNullException(nameof(bookToAdd)); 55 | } 56 | 57 | _context.Add(bookToAdd); 58 | } 59 | 60 | public async Task SaveChangesAsync() 61 | { 62 | // return true if 1 or more entities were changed 63 | return (await _context.SaveChangesAsync() > 0); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Starter files/Library.API/Services/IAuthorRepository.cs: -------------------------------------------------------------------------------- 1 | using Library.API.Entities; 2 | 3 | namespace Library.API.Services 4 | { 5 | public interface IAuthorRepository 6 | { 7 | Task AuthorExistsAsync(Guid authorId); 8 | 9 | Task> GetAuthorsAsync(); 10 | 11 | Task GetAuthorAsync(Guid authorId); 12 | 13 | void UpdateAuthor(Author author); 14 | 15 | Task SaveChangesAsync(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Starter files/Library.API/Services/IBookRepository.cs: -------------------------------------------------------------------------------- 1 | using Library.API.Entities; 2 | 3 | namespace Library.API.Services 4 | { 5 | public interface IBookRepository 6 | { 7 | Task> GetBooksAsync(Guid authorId); 8 | 9 | Task GetBookAsync(Guid authorId, Guid bookId); 10 | 11 | void AddBook(Book bookToAdd); 12 | 13 | Task SaveChangesAsync(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Starter files/Library.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Starter files/Library.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "ConnectionStrings": { 10 | "LibraryDBConnectionString": "Data Source=LibraryAPI.db" 11 | } 12 | } 13 | --------------------------------------------------------------------------------