├── CityInfo.API ├── CityInfo.db ├── appSettings.Production.json ├── getting-started-with-rest-slides.pdf ├── Services │ ├── IMailService.cs │ ├── PaginationMetadata.cs │ ├── CloudMailService.cs │ ├── LocalMailService.cs │ ├── ICityInfoRepository.cs │ └── CityInfoRepository.cs ├── Models │ ├── PointOfInterestDto.cs │ ├── PointOfInterestForCreationDto.cs │ ├── PointOfInterestForUpdateDto.cs │ ├── CityDto.cs │ └── CityWithoutPointsOfInterestDto.cs ├── appsettings.json ├── Profiles │ ├── CityProfile.cs │ └── PointOfInterestProfile.cs ├── appsettings.Development.json ├── Entities │ ├── City.cs │ └── PointOfInterest.cs ├── Properties │ └── launchSettings.json ├── Migrations │ ├── 20211221143636_CityInfoDBAddPointOfInterestDescription.cs │ ├── 20211221142239_CityInfoDBInitialMigration.cs │ ├── 20211221142239_CityInfoDBInitialMigration.Designer.cs │ ├── 20211221143636_CityInfoDBAddPointOfInterestDescription.Designer.cs │ ├── 20211221144759_DataSeed.cs │ ├── CityInfoContextModelSnapshot.cs │ └── 20211221144759_DataSeed.Designer.cs ├── CityInfo.API.xml ├── Controllers │ ├── FilesController.cs │ ├── CitiesController.cs │ ├── AuthenticationController.cs │ └── PointsOfInterestController.cs ├── CityInfo.API.csproj ├── CitiesDataStore.cs ├── DbContexts │ └── CityInfoContext.cs └── Program.cs ├── README.md ├── LICENSE ├── CityInfo.sln ├── .gitignore └── ASP.NET Core 6 Web API Fundamentals.postman_collection.json /CityInfo.API/CityInfo.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinDockx/AspNetCore6WebAPIFundamentals/HEAD/CityInfo.API/CityInfo.db -------------------------------------------------------------------------------- /CityInfo.API/appSettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "mailSettings": { 3 | "mailToAddress": "admin@mycompany.com" 4 | } 5 | } -------------------------------------------------------------------------------- /CityInfo.API/getting-started-with-rest-slides.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinDockx/AspNetCore6WebAPIFundamentals/HEAD/CityInfo.API/getting-started-with-rest-slides.pdf -------------------------------------------------------------------------------- /CityInfo.API/Services/IMailService.cs: -------------------------------------------------------------------------------- 1 | namespace CityInfo.API.Services 2 | { 3 | public interface IMailService 4 | { 5 | void Send(string subject, string message); 6 | } 7 | } -------------------------------------------------------------------------------- /CityInfo.API/Models/PointOfInterestDto.cs: -------------------------------------------------------------------------------- 1 | namespace CityInfo.API.Models 2 | { 3 | public class PointOfInterestDto 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } = string.Empty; 7 | public string? Description { get; set; } 8 | 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASP.NET Core 6 Web API Fundamentals 2 | Fully functioning sample code for my ASP.NET Core 6 Web API Fundamentals course at Pluralsight. 3 | 4 | Tackles fundamental concerns like CRUD, dependency injection, connecting to a database, authentication, versioning & documenting your API, and much more. 5 | -------------------------------------------------------------------------------- /CityInfo.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "mailSettings": { 9 | "mailToAddress": "developer@mycompany.com", 10 | "mailFromAddress": "noreply@mycompany.com" 11 | }, 12 | "AllowedHosts": "*" 13 | } 14 | -------------------------------------------------------------------------------- /CityInfo.API/Profiles/CityProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace CityInfo.API.Profiles 4 | { 5 | public class CityProfile : Profile 6 | { 7 | public CityProfile() 8 | { 9 | CreateMap(); 10 | CreateMap(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /CityInfo.API/Models/PointOfInterestForCreationDto.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace CityInfo.API.Models 4 | { 5 | public class PointOfInterestForCreationDto 6 | { 7 | [Required(ErrorMessage = "You should provide a name value.")] 8 | [MaxLength(50)] 9 | public string Name { get; set; } = string.Empty; 10 | 11 | [MaxLength(200)] 12 | public string? Description { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /CityInfo.API/Models/PointOfInterestForUpdateDto.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace CityInfo.API.Models 4 | { 5 | public class PointOfInterestForUpdateDto 6 | { 7 | [Required(ErrorMessage = "You should provide a name value.")] 8 | [MaxLength(50)] 9 | public string Name { get; set; } = string.Empty; 10 | 11 | [MaxLength(200)] 12 | public string? Description { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /CityInfo.API/Models/CityDto.cs: -------------------------------------------------------------------------------- 1 | namespace CityInfo.API.Models 2 | { 3 | public class CityDto 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } = string.Empty; 7 | public string? Description { get; set; } 8 | public int NumberOfPointsOfInterest 9 | { 10 | get 11 | { 12 | return PointsOfInterest.Count; 13 | } 14 | } 15 | 16 | public ICollection PointsOfInterest { get; set; } 17 | = new List(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CityInfo.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "CityInfo.API.Controllers": "Information", 6 | "Microsoft.AspNetCore": "Warning", 7 | "Microsoft.EntityFrameworkCore.Database.Command": "Information" 8 | } 9 | }, 10 | "ConnectionStrings": { 11 | "CityInfoDBConnectionString": "Data Source=CityInfo.db" 12 | }, 13 | "Authentication": { 14 | "SecretForKey": "thisisthesecretforgeneratingakey(mustbeatleast32bitlong)", 15 | "Issuer": "https://localhost:7169", 16 | "Audience": "cityinfoapi" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CityInfo.API/Profiles/PointOfInterestProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace CityInfo.API.Profiles 4 | { 5 | public class PointOfInterestProfile : Profile 6 | { 7 | public PointOfInterestProfile() 8 | { 9 | CreateMap(); 10 | CreateMap(); 11 | CreateMap(); 12 | CreateMap(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /CityInfo.API/Services/PaginationMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace CityInfo.API.Services 2 | { 3 | public class PaginationMetadata 4 | { 5 | public int TotalItemCount { get; set; } 6 | public int TotalPageCount { get; set; } 7 | public int PageSize { get; set; } 8 | public int CurrentPage { get; set; } 9 | 10 | public PaginationMetadata(int totalItemCount, int pageSize, int currentPage) 11 | { 12 | TotalItemCount = totalItemCount; 13 | PageSize = pageSize; 14 | CurrentPage = currentPage; 15 | TotalPageCount = (int)Math.Ceiling(totalItemCount / (double)pageSize); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CityInfo.API/Models/CityWithoutPointsOfInterestDto.cs: -------------------------------------------------------------------------------- 1 | namespace CityInfo.API.Models 2 | { 3 | /// 4 | /// A DTO for a city without points of interest 5 | /// 6 | public class CityWithoutPointsOfInterestDto 7 | { 8 | /// 9 | /// The id of the city 10 | /// 11 | public int Id { get; set; } 12 | 13 | /// 14 | /// The name of the city 15 | /// 16 | public string Name { get; set; } = string.Empty; 17 | 18 | /// 19 | /// The description of the city 20 | /// 21 | public string? Description { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CityInfo.API/Entities/City.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace CityInfo.API.Entities 5 | { 6 | public class City 7 | { 8 | [Key] 9 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 10 | public int Id { get; set; } 11 | 12 | [Required] 13 | [MaxLength(50)] 14 | public string Name { get; set; } 15 | 16 | [MaxLength(200)] 17 | public string? Description { get; set; } 18 | 19 | public ICollection PointsOfInterest { get; set; } 20 | = new List(); 21 | 22 | public City(string name) 23 | { 24 | Name = name; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /CityInfo.API/Entities/PointOfInterest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace CityInfo.API.Entities 5 | { 6 | public class PointOfInterest 7 | { 8 | [Key] 9 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 10 | public int Id { get; set; } 11 | 12 | [Required] 13 | [MaxLength(50)] 14 | public string Name { get; set; } 15 | 16 | [MaxLength(200)] 17 | public string Description { get; set; } 18 | 19 | 20 | [ForeignKey("CityId")] 21 | public City? City { get; set; } 22 | public int CityId { get; set; } 23 | 24 | public PointOfInterest(string name) 25 | { 26 | Name = name; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /CityInfo.API/Services/CloudMailService.cs: -------------------------------------------------------------------------------- 1 | namespace CityInfo.API.Services 2 | { 3 | public class CloudMailService : IMailService 4 | { 5 | private readonly string _mailTo = string.Empty; 6 | private readonly string _mailFrom = string.Empty; 7 | 8 | public CloudMailService(IConfiguration configuration) 9 | { 10 | _mailTo = configuration["mailSettings:mailToAddress"]; 11 | _mailFrom = configuration["mailSettings:mailFromAddress"]; 12 | } 13 | 14 | public void Send(string subject, string message) 15 | { 16 | // send mail - output to console window 17 | Console.WriteLine($"Mail from {_mailFrom} to {_mailTo}, " + 18 | $"with {nameof(CloudMailService)}."); 19 | Console.WriteLine($"Subject: {subject}"); 20 | Console.WriteLine($"Message: {message}"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CityInfo.API/Services/LocalMailService.cs: -------------------------------------------------------------------------------- 1 | namespace CityInfo.API.Services 2 | { 3 | public class LocalMailService : IMailService 4 | { 5 | private readonly string _mailTo = string.Empty; 6 | private readonly string _mailFrom = string.Empty; 7 | 8 | public LocalMailService(IConfiguration configuration) 9 | { 10 | _mailTo = configuration["mailSettings:mailToAddress"]; 11 | _mailFrom = configuration["mailSettings:mailFromAddress"]; 12 | } 13 | 14 | public void Send(string subject, string message) 15 | { 16 | // send mail - output to console window 17 | Console.WriteLine($"Mail from {_mailFrom} to {_mailTo}, " + 18 | $"with {nameof(LocalMailService)}."); 19 | Console.WriteLine($"Subject: {subject}"); 20 | Console.WriteLine($"Message: {message}"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CityInfo.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:37419", 8 | "sslPort": 44355 9 | } 10 | }, 11 | "profiles": { 12 | "CityInfo.API": { 13 | "commandName": "Project", 14 | "launchUrl": "swagger", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | }, 18 | "applicationUrl": "https://localhost:7169;http://localhost:5169", 19 | "dotnetRunMessages": true 20 | }, 21 | "IIS Express": { 22 | "commandName": "IISExpress", 23 | "launchBrowser": true, 24 | "launchUrl": "swagger", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /CityInfo.API/Migrations/20211221143636_CityInfoDBAddPointOfInterestDescription.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace CityInfo.API.Migrations 6 | { 7 | public partial class CityInfoDBAddPointOfInterestDescription : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.AddColumn( 12 | name: "Description", 13 | table: "PointsOfInterest", 14 | type: "TEXT", 15 | maxLength: 200, 16 | nullable: false, 17 | defaultValue: ""); 18 | } 19 | 20 | protected override void Down(MigrationBuilder migrationBuilder) 21 | { 22 | migrationBuilder.DropColumn( 23 | name: "Description", 24 | table: "PointsOfInterest"); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /CityInfo.API/Services/ICityInfoRepository.cs: -------------------------------------------------------------------------------- 1 | using CityInfo.API.Entities; 2 | 3 | namespace CityInfo.API.Services 4 | { 5 | public interface ICityInfoRepository 6 | { 7 | Task> GetCitiesAsync(); 8 | Task<(IEnumerable, PaginationMetadata)> GetCitiesAsync( 9 | string? name, string? searchQuery, int pageNumber, int pageSize); 10 | Task GetCityAsync(int cityId, bool includePointsOfInterest); 11 | Task CityExistsAsync(int cityId); 12 | Task> GetPointsOfInterestForCityAsync(int cityId); 13 | Task GetPointOfInterestForCityAsync(int cityId, 14 | int pointOfInterestId); 15 | Task AddPointOfInterestForCityAsync(int cityId, PointOfInterest pointOfInterest); 16 | void DeletePointOfInterest(PointOfInterest pointOfInterest); 17 | Task CityNameMatchesCityId(string? cityName, int cityId); 18 | Task SaveChangesAsync(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kevin Dockx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CityInfo.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}") = "CityInfo.API", "CityInfo.API\CityInfo.API.csproj", "{2E9DBB4F-88CA-41AC-9F0B-BC21AA23ADEE}" 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 | {2E9DBB4F-88CA-41AC-9F0B-BC21AA23ADEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {2E9DBB4F-88CA-41AC-9F0B-BC21AA23ADEE}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {2E9DBB4F-88CA-41AC-9F0B-BC21AA23ADEE}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {2E9DBB4F-88CA-41AC-9F0B-BC21AA23ADEE}.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 = {8246798E-74EA-4D7A-9E06-5B8930FFBD43} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /CityInfo.API/CityInfo.API.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CityInfo.API 5 | 6 | 7 | 8 | 9 | Get a city by id 10 | 11 | The id of the city to get 12 | Whether or not to include the points of interest 13 | An IActionResult 14 | Returns the requested city 15 | 16 | 17 | 18 | A DTO for a city without points of interest 19 | 20 | 21 | 22 | 23 | The id of the city 24 | 25 | 26 | 27 | 28 | The name of the city 29 | 30 | 31 | 32 | 33 | The description of the city 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /CityInfo.API/Controllers/FilesController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.StaticFiles; 5 | 6 | namespace CityInfo.API.Controllers 7 | { 8 | [Route("api/files")] 9 | [Authorize] 10 | [ApiController] 11 | public class FilesController : ControllerBase 12 | { 13 | 14 | private readonly FileExtensionContentTypeProvider _fileExtensionContentTypeProvider; 15 | 16 | public FilesController( 17 | FileExtensionContentTypeProvider fileExtensionContentTypeProvider) 18 | { 19 | _fileExtensionContentTypeProvider = fileExtensionContentTypeProvider 20 | ?? throw new System.ArgumentNullException( 21 | nameof(fileExtensionContentTypeProvider)); 22 | } 23 | 24 | [HttpGet("{fileId}")] 25 | public ActionResult GetFile(string fileId) 26 | { 27 | // look up the actual file, depending on the fileId... 28 | // demo code 29 | var pathToFile = "getting-started-with-rest-slides.pdf"; 30 | 31 | // check whether the file exists 32 | if (!System.IO.File.Exists(pathToFile)) 33 | { 34 | return NotFound(); 35 | } 36 | 37 | if (!_fileExtensionContentTypeProvider.TryGetContentType( 38 | pathToFile, out var contentType)) 39 | { 40 | contentType = "application/octet-stream"; 41 | } 42 | 43 | var bytes = System.IO.File.ReadAllBytes(pathToFile); 44 | return File(bytes, contentType, Path.GetFileName(pathToFile)); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CityInfo.API/CityInfo.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | True 8 | CityInfo.API.xml 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | Always 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /CityInfo.API/Migrations/20211221142239_CityInfoDBInitialMigration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace CityInfo.API.Migrations 6 | { 7 | public partial class CityInfoDBInitialMigration : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "Cities", 13 | columns: table => new 14 | { 15 | Id = table.Column(type: "INTEGER", nullable: false) 16 | .Annotation("Sqlite:Autoincrement", true), 17 | Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), 18 | Description = table.Column(type: "TEXT", maxLength: 200, nullable: true) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Cities", x => x.Id); 23 | }); 24 | 25 | migrationBuilder.CreateTable( 26 | name: "PointsOfInterest", 27 | columns: table => new 28 | { 29 | Id = table.Column(type: "INTEGER", nullable: false) 30 | .Annotation("Sqlite:Autoincrement", true), 31 | Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), 32 | CityId = table.Column(type: "INTEGER", nullable: false) 33 | }, 34 | constraints: table => 35 | { 36 | table.PrimaryKey("PK_PointsOfInterest", x => x.Id); 37 | table.ForeignKey( 38 | name: "FK_PointsOfInterest_Cities_CityId", 39 | column: x => x.CityId, 40 | principalTable: "Cities", 41 | principalColumn: "Id", 42 | onDelete: ReferentialAction.Cascade); 43 | }); 44 | 45 | migrationBuilder.CreateIndex( 46 | name: "IX_PointsOfInterest_CityId", 47 | table: "PointsOfInterest", 48 | column: "CityId"); 49 | } 50 | 51 | protected override void Down(MigrationBuilder migrationBuilder) 52 | { 53 | migrationBuilder.DropTable( 54 | name: "PointsOfInterest"); 55 | 56 | migrationBuilder.DropTable( 57 | name: "Cities"); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /CityInfo.API/Migrations/20211221142239_CityInfoDBInitialMigration.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using CityInfo.API.DbContexts; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | 8 | #nullable disable 9 | 10 | namespace CityInfo.API.Migrations 11 | { 12 | [DbContext(typeof(CityInfoContext))] 13 | [Migration("20211221142239_CityInfoDBInitialMigration")] 14 | partial class CityInfoDBInitialMigration 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder.HasAnnotation("ProductVersion", "6.0.0"); 20 | 21 | modelBuilder.Entity("CityInfo.API.Entities.City", b => 22 | { 23 | b.Property("Id") 24 | .ValueGeneratedOnAdd() 25 | .HasColumnType("INTEGER"); 26 | 27 | b.Property("Description") 28 | .HasMaxLength(200) 29 | .HasColumnType("TEXT"); 30 | 31 | b.Property("Name") 32 | .IsRequired() 33 | .HasMaxLength(50) 34 | .HasColumnType("TEXT"); 35 | 36 | b.HasKey("Id"); 37 | 38 | b.ToTable("Cities"); 39 | }); 40 | 41 | modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b => 42 | { 43 | b.Property("Id") 44 | .ValueGeneratedOnAdd() 45 | .HasColumnType("INTEGER"); 46 | 47 | b.Property("CityId") 48 | .HasColumnType("INTEGER"); 49 | 50 | b.Property("Name") 51 | .IsRequired() 52 | .HasMaxLength(50) 53 | .HasColumnType("TEXT"); 54 | 55 | b.HasKey("Id"); 56 | 57 | b.HasIndex("CityId"); 58 | 59 | b.ToTable("PointsOfInterest"); 60 | }); 61 | 62 | modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b => 63 | { 64 | b.HasOne("CityInfo.API.Entities.City", "City") 65 | .WithMany("PointsOfInterest") 66 | .HasForeignKey("CityId") 67 | .OnDelete(DeleteBehavior.Cascade) 68 | .IsRequired(); 69 | 70 | b.Navigation("City"); 71 | }); 72 | 73 | modelBuilder.Entity("CityInfo.API.Entities.City", b => 74 | { 75 | b.Navigation("PointsOfInterest"); 76 | }); 77 | #pragma warning restore 612, 618 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /CityInfo.API/Controllers/CitiesController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CityInfo.API.Models; 3 | using CityInfo.API.Services; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | using System.Text.Json; 7 | 8 | namespace CityInfo.API.Controllers 9 | { 10 | [ApiController] 11 | [Authorize] 12 | [ApiVersion("1.0")] 13 | [ApiVersion("2.0")] 14 | [Route("api/v{version:apiVersion}/cities")] 15 | public class CitiesController : ControllerBase 16 | { 17 | private readonly ICityInfoRepository _cityInfoRepository; 18 | private readonly IMapper _mapper; 19 | const int maxCitiesPageSize = 20; 20 | 21 | public CitiesController(ICityInfoRepository cityInfoRepository, 22 | IMapper mapper) 23 | { 24 | _cityInfoRepository = cityInfoRepository ?? 25 | throw new ArgumentNullException(nameof(cityInfoRepository)); 26 | _mapper = mapper ?? 27 | throw new ArgumentNullException(nameof(mapper)); 28 | } 29 | 30 | [HttpGet] 31 | public async Task>> GetCities( 32 | string? name, string? searchQuery, int pageNumber = 1, int pageSize = 10) 33 | { 34 | if (pageSize > maxCitiesPageSize) 35 | { 36 | pageSize = maxCitiesPageSize; 37 | } 38 | 39 | var (cityEntities, paginationMetadata) = await _cityInfoRepository 40 | .GetCitiesAsync(name, searchQuery, pageNumber, pageSize); 41 | 42 | Response.Headers.Add("X-Pagination", 43 | JsonSerializer.Serialize(paginationMetadata)); 44 | 45 | return Ok(_mapper.Map>(cityEntities)); 46 | } 47 | 48 | /// 49 | /// Get a city by id 50 | /// 51 | /// The id of the city to get 52 | /// Whether or not to include the points of interest 53 | /// An IActionResult 54 | /// Returns the requested city 55 | [HttpGet("{id}")] 56 | [ProducesResponseType(StatusCodes.Status200OK)] 57 | [ProducesResponseType(StatusCodes.Status404NotFound)] 58 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 59 | public async Task GetCity( 60 | int id, bool includePointsOfInterest = false) 61 | { 62 | var city = await _cityInfoRepository.GetCityAsync(id, includePointsOfInterest); 63 | if (city == null) 64 | { 65 | return NotFound(); 66 | } 67 | 68 | if (includePointsOfInterest) 69 | { 70 | return Ok(_mapper.Map(city)); 71 | } 72 | 73 | return Ok(_mapper.Map(city)); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /CityInfo.API/CitiesDataStore.cs: -------------------------------------------------------------------------------- 1 | using CityInfo.API.Models; 2 | 3 | namespace CityInfo.API 4 | { 5 | public class CitiesDataStore 6 | { 7 | public List Cities { get; set; } 8 | // public static CitiesDataStore Current { get; } = new CitiesDataStore(); 9 | 10 | public CitiesDataStore() 11 | { 12 | // init dummy data 13 | Cities = new List() 14 | { 15 | new CityDto() 16 | { 17 | Id = 1, 18 | Name = "New York City", 19 | Description = "The one with that big park.", 20 | PointsOfInterest = new List() 21 | { 22 | new PointOfInterestDto() { 23 | Id = 1, 24 | Name = "Central Park", 25 | Description = "The most visited urban park in the United States." }, 26 | new PointOfInterestDto() { 27 | Id = 2, 28 | Name = "Empire State Building", 29 | Description = "A 102-story skyscraper located in Midtown Manhattan." }, 30 | } 31 | }, 32 | new CityDto() 33 | { 34 | Id = 2, 35 | Name = "Antwerp", 36 | Description = "The one with the cathedral that was never really finished.", 37 | PointsOfInterest = new List() 38 | { 39 | new PointOfInterestDto() { 40 | Id = 3, 41 | Name = "Cathedral of Our Lady", 42 | Description = "A Gothic style cathedral, conceived by architects Jan and Pieter Appelmans." }, 43 | new PointOfInterestDto() { 44 | Id = 4, 45 | Name = "Antwerp Central Station", 46 | Description = "The the finest example of railway architecture in Belgium." }, 47 | } 48 | }, 49 | new CityDto() 50 | { 51 | Id= 3, 52 | Name = "Paris", 53 | Description = "The one with that big tower.", 54 | PointsOfInterest = new List() 55 | { 56 | new PointOfInterestDto() { 57 | Id = 5, 58 | Name = "Eiffel Tower", 59 | Description = "A wrought iron lattice tower on the Champ de Mars, named after engineer Gustave Eiffel." }, 60 | new PointOfInterestDto() { 61 | Id = 6, 62 | Name = "The Louvre", 63 | Description = "The world's largest museum." }, 64 | } 65 | } 66 | }; 67 | 68 | } 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /CityInfo.API/Migrations/20211221143636_CityInfoDBAddPointOfInterestDescription.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using CityInfo.API.DbContexts; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | 8 | #nullable disable 9 | 10 | namespace CityInfo.API.Migrations 11 | { 12 | [DbContext(typeof(CityInfoContext))] 13 | [Migration("20211221143636_CityInfoDBAddPointOfInterestDescription")] 14 | partial class CityInfoDBAddPointOfInterestDescription 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder.HasAnnotation("ProductVersion", "6.0.0"); 20 | 21 | modelBuilder.Entity("CityInfo.API.Entities.City", b => 22 | { 23 | b.Property("Id") 24 | .ValueGeneratedOnAdd() 25 | .HasColumnType("INTEGER"); 26 | 27 | b.Property("Description") 28 | .HasMaxLength(200) 29 | .HasColumnType("TEXT"); 30 | 31 | b.Property("Name") 32 | .IsRequired() 33 | .HasMaxLength(50) 34 | .HasColumnType("TEXT"); 35 | 36 | b.HasKey("Id"); 37 | 38 | b.ToTable("Cities"); 39 | }); 40 | 41 | modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b => 42 | { 43 | b.Property("Id") 44 | .ValueGeneratedOnAdd() 45 | .HasColumnType("INTEGER"); 46 | 47 | b.Property("CityId") 48 | .HasColumnType("INTEGER"); 49 | 50 | b.Property("Description") 51 | .IsRequired() 52 | .HasMaxLength(200) 53 | .HasColumnType("TEXT"); 54 | 55 | b.Property("Name") 56 | .IsRequired() 57 | .HasMaxLength(50) 58 | .HasColumnType("TEXT"); 59 | 60 | b.HasKey("Id"); 61 | 62 | b.HasIndex("CityId"); 63 | 64 | b.ToTable("PointsOfInterest"); 65 | }); 66 | 67 | modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b => 68 | { 69 | b.HasOne("CityInfo.API.Entities.City", "City") 70 | .WithMany("PointsOfInterest") 71 | .HasForeignKey("CityId") 72 | .OnDelete(DeleteBehavior.Cascade) 73 | .IsRequired(); 74 | 75 | b.Navigation("City"); 76 | }); 77 | 78 | modelBuilder.Entity("CityInfo.API.Entities.City", b => 79 | { 80 | b.Navigation("PointsOfInterest"); 81 | }); 82 | #pragma warning restore 612, 618 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /CityInfo.API/DbContexts/CityInfoContext.cs: -------------------------------------------------------------------------------- 1 | using CityInfo.API.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace CityInfo.API.DbContexts 5 | { 6 | public class CityInfoContext : DbContext 7 | { 8 | public DbSet Cities { get; set; } = null!; 9 | public DbSet PointsOfInterest { get; set; } = null!; 10 | 11 | public CityInfoContext(DbContextOptions options) 12 | : base(options) 13 | { 14 | 15 | } 16 | 17 | protected override void OnModelCreating(ModelBuilder modelBuilder) 18 | { 19 | modelBuilder.Entity() 20 | .HasData( 21 | new City("New York City") 22 | { 23 | Id = 1, 24 | Description = "The one with that big park." 25 | }, 26 | new City("Antwerp") 27 | { 28 | Id = 2, 29 | Description = "The one with the cathedral that was never really finished." 30 | }, 31 | new City("Paris") 32 | { 33 | Id = 3, 34 | Description = "The one with that big tower." 35 | }); 36 | 37 | modelBuilder.Entity() 38 | .HasData( 39 | new PointOfInterest("Central Park") 40 | { 41 | Id = 1, 42 | CityId = 1, 43 | Description = "The most visited urban park in the United States." 44 | }, 45 | new PointOfInterest("Empire State Building") 46 | { 47 | Id = 2, 48 | CityId = 1, 49 | Description = "A 102-story skyscraper located in Midtown Manhattan." 50 | }, 51 | new PointOfInterest("Cathedral") 52 | { 53 | Id = 3, 54 | CityId = 2, 55 | Description = "A Gothic style cathedral, conceived by architects Jan and Pieter Appelmans." 56 | }, 57 | new PointOfInterest("Antwerp Central Station") 58 | { 59 | Id = 4, 60 | CityId = 2, 61 | Description = "The the finest example of railway architecture in Belgium." 62 | }, 63 | new PointOfInterest("Eiffel Tower") 64 | { 65 | Id = 5, 66 | CityId = 3, 67 | Description = "A wrought iron lattice tower on the Champ de Mars, named after engineer Gustave Eiffel." 68 | }, 69 | new PointOfInterest("The Louvre") 70 | { 71 | Id = 6, 72 | CityId = 3, 73 | Description = "The world's largest museum." 74 | } 75 | ); 76 | base.OnModelCreating(modelBuilder); 77 | } 78 | 79 | //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 80 | //{ 81 | // optionsBuilder.UseSqlite("connectionstring"); 82 | // base.OnConfiguring(optionsBuilder); 83 | //} 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /CityInfo.API/Services/CityInfoRepository.cs: -------------------------------------------------------------------------------- 1 | using CityInfo.API.DbContexts; 2 | using CityInfo.API.Entities; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace CityInfo.API.Services 6 | { 7 | public class CityInfoRepository : ICityInfoRepository 8 | { 9 | private readonly CityInfoContext _context; 10 | 11 | public CityInfoRepository(CityInfoContext context) 12 | { 13 | _context = context ?? throw new ArgumentNullException(nameof(context)); 14 | } 15 | 16 | public async Task> GetCitiesAsync() 17 | { 18 | return await _context.Cities.OrderBy(c => c.Name).ToListAsync(); 19 | } 20 | 21 | public async Task CityNameMatchesCityId(string? cityName, int cityId) 22 | { 23 | return await _context.Cities.AnyAsync(c => c.Id == cityId && c.Name == cityName); 24 | } 25 | 26 | public async Task<(IEnumerable, PaginationMetadata)> GetCitiesAsync( 27 | string? name, string? searchQuery, int pageNumber, int pageSize) 28 | { 29 | // collection to start from 30 | var collection = _context.Cities as IQueryable; 31 | 32 | if (!string.IsNullOrWhiteSpace(name)) 33 | { 34 | name = name.Trim(); 35 | collection = collection.Where(c => c.Name == name); 36 | } 37 | 38 | if (!string.IsNullOrWhiteSpace(searchQuery)) 39 | { 40 | searchQuery = searchQuery.Trim(); 41 | collection = collection.Where(a => a.Name.Contains(searchQuery) 42 | || (a.Description != null && a.Description.Contains(searchQuery))); 43 | } 44 | 45 | var totalItemCount = await collection.CountAsync(); 46 | 47 | var paginationMetadata = new PaginationMetadata( 48 | totalItemCount, pageSize, pageNumber); 49 | 50 | var collectionToReturn = await collection.OrderBy(c => c.Name) 51 | .Skip(pageSize * (pageNumber - 1)) 52 | .Take(pageSize) 53 | .ToListAsync(); 54 | 55 | return (collectionToReturn, paginationMetadata); 56 | } 57 | 58 | 59 | 60 | public async Task GetCityAsync(int cityId, bool includePointsOfInterest) 61 | { 62 | if (includePointsOfInterest) 63 | { 64 | return await _context.Cities.Include(c => c.PointsOfInterest) 65 | .Where(c => c.Id == cityId).FirstOrDefaultAsync(); 66 | } 67 | 68 | return await _context.Cities 69 | .Where(c => c.Id == cityId).FirstOrDefaultAsync(); 70 | } 71 | 72 | public async Task CityExistsAsync(int cityId) 73 | { 74 | return await _context.Cities.AnyAsync(c => c.Id == cityId); 75 | } 76 | 77 | public async Task GetPointOfInterestForCityAsync( 78 | int cityId, 79 | int pointOfInterestId) 80 | { 81 | return await _context.PointsOfInterest 82 | .Where(p => p.CityId == cityId && p.Id == pointOfInterestId) 83 | .FirstOrDefaultAsync(); 84 | } 85 | 86 | public async Task> GetPointsOfInterestForCityAsync( 87 | int cityId) 88 | { 89 | return await _context.PointsOfInterest 90 | .Where(p => p.CityId == cityId).ToListAsync(); 91 | } 92 | 93 | public async Task AddPointOfInterestForCityAsync(int cityId, 94 | PointOfInterest pointOfInterest) 95 | { 96 | var city = await GetCityAsync(cityId, false); 97 | if (city != null) 98 | { 99 | city.PointsOfInterest.Add(pointOfInterest); 100 | } 101 | } 102 | 103 | public void DeletePointOfInterest(PointOfInterest pointOfInterest) 104 | { 105 | _context.PointsOfInterest.Remove(pointOfInterest); 106 | } 107 | 108 | public async Task SaveChangesAsync() 109 | { 110 | return (await _context.SaveChangesAsync() >= 0); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /CityInfo.API/Controllers/AuthenticationController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.IdentityModel.Tokens; 4 | using System.IdentityModel.Tokens.Jwt; 5 | using System.Security.Claims; 6 | using System.Text; 7 | 8 | namespace CityInfo.API.Controllers 9 | { 10 | [Route("api/authentication")] 11 | [ApiController] 12 | public class AuthenticationController : ControllerBase 13 | { 14 | private readonly IConfiguration _configuration; 15 | 16 | // we won't use this outside of this class, so we can scope it to this namespace 17 | public class AuthenticationRequestBody 18 | { 19 | public string? UserName { get; set; } 20 | public string? Password { get; set; } 21 | } 22 | 23 | private class CityInfoUser 24 | { 25 | public int UserId { get; set; } 26 | public string UserName { get; set; } 27 | public string FirstName { get; set; } 28 | public string LastName { get; set; } 29 | public string City { get; set; } 30 | 31 | public CityInfoUser( 32 | int userId, 33 | string userName, 34 | string firstName, 35 | string lastName, 36 | string city) 37 | { 38 | UserId = userId; 39 | UserName = userName; 40 | FirstName = firstName; 41 | LastName = lastName; 42 | City = city; 43 | } 44 | 45 | } 46 | 47 | public AuthenticationController(IConfiguration configuration) 48 | { 49 | _configuration = configuration ?? 50 | throw new ArgumentNullException(nameof(configuration)); 51 | } 52 | 53 | [HttpPost("authenticate")] 54 | public ActionResult Authenticate( 55 | AuthenticationRequestBody authenticationRequestBody) 56 | { 57 | // Step 1: validate the username/password 58 | var user = ValidateUserCredentials( 59 | authenticationRequestBody.UserName, 60 | authenticationRequestBody.Password); 61 | 62 | if (user == null) 63 | { 64 | return Unauthorized(); 65 | } 66 | 67 | // Step 2: create a token 68 | var securityKey = new SymmetricSecurityKey( 69 | Encoding.ASCII.GetBytes(_configuration["Authentication:SecretForKey"])); 70 | var signingCredentials = new SigningCredentials( 71 | securityKey, SecurityAlgorithms.HmacSha256); 72 | 73 | var claimsForToken = new List(); 74 | claimsForToken.Add(new Claim("sub", user.UserId.ToString())); 75 | claimsForToken.Add(new Claim("given_name", user.FirstName)); 76 | claimsForToken.Add(new Claim("family_name", user.LastName)); 77 | claimsForToken.Add(new Claim("city", user.City)); 78 | 79 | var jwtSecurityToken = new JwtSecurityToken( 80 | _configuration["Authentication:Issuer"], 81 | _configuration["Authentication:Audience"], 82 | claimsForToken, 83 | DateTime.UtcNow, 84 | DateTime.UtcNow.AddHours(1), 85 | signingCredentials); 86 | 87 | var tokenToReturn = new JwtSecurityTokenHandler() 88 | .WriteToken(jwtSecurityToken); 89 | 90 | return Ok(tokenToReturn); 91 | } 92 | 93 | private CityInfoUser ValidateUserCredentials(string? userName, string? password) 94 | { 95 | // we don't have a user DB or table. If you have, check the passed-through 96 | // username/password against what's stored in the database. 97 | // 98 | // For demo purposes, we assume the credentials are valid 99 | 100 | // return a new CityInfoUser (values would normally come from your user DB/table) 101 | return new CityInfoUser( 102 | 1, 103 | userName ?? "", 104 | "Kevin", 105 | "Dockx", 106 | "Antwerp"); 107 | 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /CityInfo.API/Program.cs: -------------------------------------------------------------------------------- 1 | using CityInfo.API; 2 | using CityInfo.API.DbContexts; 3 | using CityInfo.API.Services; 4 | using Microsoft.AspNetCore.StaticFiles; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.IdentityModel.Tokens; 7 | using Microsoft.OpenApi.Models; 8 | using Serilog; 9 | using System.Reflection; 10 | using System.Text; 11 | 12 | Log.Logger = new LoggerConfiguration() 13 | .MinimumLevel.Debug() 14 | .WriteTo.Console() 15 | .WriteTo.File("logs/cityinfo.txt", rollingInterval: RollingInterval.Day) 16 | .CreateLogger(); 17 | 18 | var builder = WebApplication.CreateBuilder(args); 19 | //builder.Logging.ClearProviders(); 20 | //builder.Logging.AddConsole(); 21 | 22 | builder.Host.UseSerilog(); 23 | 24 | // Add services to the container. 25 | 26 | builder.Services.AddControllers(options => 27 | { 28 | options.ReturnHttpNotAcceptable = true; 29 | }).AddNewtonsoftJson() 30 | .AddXmlDataContractSerializerFormatters(); 31 | 32 | 33 | // Learn more about configuring Swagger/OpenAPI at 34 | // https://aka.ms/aspnetcore/swashbuckle 35 | builder.Services.AddEndpointsApiExplorer(); 36 | builder.Services.AddSwaggerGen(setupAction => 37 | { 38 | var xmlCommentsFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; 39 | var xmlCommentsFullPath = Path.Combine(AppContext.BaseDirectory, xmlCommentsFile); 40 | 41 | setupAction.IncludeXmlComments(xmlCommentsFullPath); 42 | 43 | setupAction.AddSecurityDefinition("CityInfoApiBearerAuth", new OpenApiSecurityScheme() 44 | { 45 | Type = SecuritySchemeType.Http, 46 | Scheme = "Bearer", 47 | Description = "Input a valid token to access this API" 48 | }); 49 | 50 | setupAction.AddSecurityRequirement(new OpenApiSecurityRequirement 51 | { 52 | { 53 | new OpenApiSecurityScheme 54 | { 55 | Reference = new OpenApiReference { 56 | Type = ReferenceType.SecurityScheme, 57 | Id = "CityInfoApiBearerAuth" } 58 | }, new List() } 59 | }); 60 | }); 61 | 62 | builder.Services.AddSingleton(); 63 | 64 | #if DEBUG 65 | builder.Services.AddTransient(); 66 | #else 67 | builder.Services.AddTransient(); 68 | #endif 69 | 70 | builder.Services.AddSingleton(); 71 | 72 | builder.Services.AddDbContext( 73 | dbContextOptions => dbContextOptions.UseSqlite( 74 | builder.Configuration["ConnectionStrings:CityInfoDBConnectionString"])); 75 | 76 | builder.Services.AddScoped(); 77 | 78 | builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); 79 | 80 | builder.Services.AddAuthentication("Bearer") 81 | .AddJwtBearer(options => 82 | { 83 | options.TokenValidationParameters = new() 84 | { 85 | ValidateIssuer = true, 86 | ValidateAudience = true, 87 | ValidateIssuerSigningKey = true, 88 | ValidIssuer = builder.Configuration["Authentication:Issuer"], 89 | ValidAudience = builder.Configuration["Authentication:Audience"], 90 | IssuerSigningKey = new SymmetricSecurityKey( 91 | Encoding.ASCII.GetBytes(builder.Configuration["Authentication:SecretForKey"])) 92 | }; 93 | } 94 | ); 95 | 96 | builder.Services.AddAuthorization(options => 97 | { 98 | options.AddPolicy("MustBeFromAntwerp", policy => 99 | { 100 | policy.RequireAuthenticatedUser(); 101 | policy.RequireClaim("city", "Antwerp"); 102 | }); 103 | }); 104 | 105 | builder.Services.AddApiVersioning(setupAction => 106 | { 107 | setupAction.AssumeDefaultVersionWhenUnspecified = true; 108 | setupAction.DefaultApiVersion = new Microsoft.AspNetCore.Mvc.ApiVersion(1, 0); 109 | setupAction.ReportApiVersions = true; 110 | }); 111 | 112 | var app = builder.Build(); 113 | 114 | // Configure the HTTP request pipeline. 115 | if (app.Environment.IsDevelopment()) 116 | { 117 | app.UseSwagger(); 118 | app.UseSwaggerUI(); 119 | } 120 | 121 | app.UseHttpsRedirection(); 122 | 123 | app.UseRouting(); 124 | 125 | app.UseAuthentication(); 126 | 127 | app.UseAuthorization(); 128 | 129 | app.UseEndpoints(endpoints => 130 | { 131 | endpoints.MapControllers(); 132 | }); 133 | 134 | app.Run(); 135 | -------------------------------------------------------------------------------- /CityInfo.API/Migrations/20211221144759_DataSeed.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace CityInfo.API.Migrations 6 | { 7 | public partial class DataSeed : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.InsertData( 12 | table: "Cities", 13 | columns: new[] { "Id", "Description", "Name" }, 14 | values: new object[] { 1, "The one with that big park.", "New York City" }); 15 | 16 | migrationBuilder.InsertData( 17 | table: "Cities", 18 | columns: new[] { "Id", "Description", "Name" }, 19 | values: new object[] { 2, "The one with the cathedral that was never really finished.", "Antwerp" }); 20 | 21 | migrationBuilder.InsertData( 22 | table: "Cities", 23 | columns: new[] { "Id", "Description", "Name" }, 24 | values: new object[] { 3, "The one with that big tower.", "Paris" }); 25 | 26 | migrationBuilder.InsertData( 27 | table: "PointsOfInterest", 28 | columns: new[] { "Id", "CityId", "Description", "Name" }, 29 | values: new object[] { 1, 1, "The most visited urban park in the United States.", "Central Park" }); 30 | 31 | migrationBuilder.InsertData( 32 | table: "PointsOfInterest", 33 | columns: new[] { "Id", "CityId", "Description", "Name" }, 34 | values: new object[] { 2, 1, "A 102-story skyscraper located in Midtown Manhattan.", "Empire State Building" }); 35 | 36 | migrationBuilder.InsertData( 37 | table: "PointsOfInterest", 38 | columns: new[] { "Id", "CityId", "Description", "Name" }, 39 | values: new object[] { 3, 2, "A Gothic style cathedral, conceived by architects Jan and Pieter Appelmans.", "Cathedral" }); 40 | 41 | migrationBuilder.InsertData( 42 | table: "PointsOfInterest", 43 | columns: new[] { "Id", "CityId", "Description", "Name" }, 44 | values: new object[] { 4, 2, "The the finest example of railway architecture in Belgium.", "Antwerp Central Station" }); 45 | 46 | migrationBuilder.InsertData( 47 | table: "PointsOfInterest", 48 | columns: new[] { "Id", "CityId", "Description", "Name" }, 49 | values: new object[] { 5, 3, "A wrought iron lattice tower on the Champ de Mars, named after engineer Gustave Eiffel.", "Eiffel Tower" }); 50 | 51 | migrationBuilder.InsertData( 52 | table: "PointsOfInterest", 53 | columns: new[] { "Id", "CityId", "Description", "Name" }, 54 | values: new object[] { 6, 3, "The world's largest museum.", "The Louvre" }); 55 | } 56 | 57 | protected override void Down(MigrationBuilder migrationBuilder) 58 | { 59 | migrationBuilder.DeleteData( 60 | table: "PointsOfInterest", 61 | keyColumn: "Id", 62 | keyValue: 1); 63 | 64 | migrationBuilder.DeleteData( 65 | table: "PointsOfInterest", 66 | keyColumn: "Id", 67 | keyValue: 2); 68 | 69 | migrationBuilder.DeleteData( 70 | table: "PointsOfInterest", 71 | keyColumn: "Id", 72 | keyValue: 3); 73 | 74 | migrationBuilder.DeleteData( 75 | table: "PointsOfInterest", 76 | keyColumn: "Id", 77 | keyValue: 4); 78 | 79 | migrationBuilder.DeleteData( 80 | table: "PointsOfInterest", 81 | keyColumn: "Id", 82 | keyValue: 5); 83 | 84 | migrationBuilder.DeleteData( 85 | table: "PointsOfInterest", 86 | keyColumn: "Id", 87 | keyValue: 6); 88 | 89 | migrationBuilder.DeleteData( 90 | table: "Cities", 91 | keyColumn: "Id", 92 | keyValue: 1); 93 | 94 | migrationBuilder.DeleteData( 95 | table: "Cities", 96 | keyColumn: "Id", 97 | keyValue: 2); 98 | 99 | migrationBuilder.DeleteData( 100 | table: "Cities", 101 | keyColumn: "Id", 102 | keyValue: 3); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /CityInfo.API/Migrations/CityInfoContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using CityInfo.API.DbContexts; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | 7 | #nullable disable 8 | 9 | namespace CityInfo.API.Migrations 10 | { 11 | [DbContext(typeof(CityInfoContext))] 12 | partial class CityInfoContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder.HasAnnotation("ProductVersion", "6.0.0"); 18 | 19 | modelBuilder.Entity("CityInfo.API.Entities.City", b => 20 | { 21 | b.Property("Id") 22 | .ValueGeneratedOnAdd() 23 | .HasColumnType("INTEGER"); 24 | 25 | b.Property("Description") 26 | .HasMaxLength(200) 27 | .HasColumnType("TEXT"); 28 | 29 | b.Property("Name") 30 | .IsRequired() 31 | .HasMaxLength(50) 32 | .HasColumnType("TEXT"); 33 | 34 | b.HasKey("Id"); 35 | 36 | b.ToTable("Cities"); 37 | 38 | b.HasData( 39 | new 40 | { 41 | Id = 1, 42 | Description = "The one with that big park.", 43 | Name = "New York City" 44 | }, 45 | new 46 | { 47 | Id = 2, 48 | Description = "The one with the cathedral that was never really finished.", 49 | Name = "Antwerp" 50 | }, 51 | new 52 | { 53 | Id = 3, 54 | Description = "The one with that big tower.", 55 | Name = "Paris" 56 | }); 57 | }); 58 | 59 | modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b => 60 | { 61 | b.Property("Id") 62 | .ValueGeneratedOnAdd() 63 | .HasColumnType("INTEGER"); 64 | 65 | b.Property("CityId") 66 | .HasColumnType("INTEGER"); 67 | 68 | b.Property("Description") 69 | .IsRequired() 70 | .HasMaxLength(200) 71 | .HasColumnType("TEXT"); 72 | 73 | b.Property("Name") 74 | .IsRequired() 75 | .HasMaxLength(50) 76 | .HasColumnType("TEXT"); 77 | 78 | b.HasKey("Id"); 79 | 80 | b.HasIndex("CityId"); 81 | 82 | b.ToTable("PointsOfInterest"); 83 | 84 | b.HasData( 85 | new 86 | { 87 | Id = 1, 88 | CityId = 1, 89 | Description = "The most visited urban park in the United States.", 90 | Name = "Central Park" 91 | }, 92 | new 93 | { 94 | Id = 2, 95 | CityId = 1, 96 | Description = "A 102-story skyscraper located in Midtown Manhattan.", 97 | Name = "Empire State Building" 98 | }, 99 | new 100 | { 101 | Id = 3, 102 | CityId = 2, 103 | Description = "A Gothic style cathedral, conceived by architects Jan and Pieter Appelmans.", 104 | Name = "Cathedral" 105 | }, 106 | new 107 | { 108 | Id = 4, 109 | CityId = 2, 110 | Description = "The the finest example of railway architecture in Belgium.", 111 | Name = "Antwerp Central Station" 112 | }, 113 | new 114 | { 115 | Id = 5, 116 | CityId = 3, 117 | Description = "A wrought iron lattice tower on the Champ de Mars, named after engineer Gustave Eiffel.", 118 | Name = "Eiffel Tower" 119 | }, 120 | new 121 | { 122 | Id = 6, 123 | CityId = 3, 124 | Description = "The world's largest museum.", 125 | Name = "The Louvre" 126 | }); 127 | }); 128 | 129 | modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b => 130 | { 131 | b.HasOne("CityInfo.API.Entities.City", "City") 132 | .WithMany("PointsOfInterest") 133 | .HasForeignKey("CityId") 134 | .OnDelete(DeleteBehavior.Cascade) 135 | .IsRequired(); 136 | 137 | b.Navigation("City"); 138 | }); 139 | 140 | modelBuilder.Entity("CityInfo.API.Entities.City", b => 141 | { 142 | b.Navigation("PointsOfInterest"); 143 | }); 144 | #pragma warning restore 612, 618 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /CityInfo.API/Migrations/20211221144759_DataSeed.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using CityInfo.API.DbContexts; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | 8 | #nullable disable 9 | 10 | namespace CityInfo.API.Migrations 11 | { 12 | [DbContext(typeof(CityInfoContext))] 13 | [Migration("20211221144759_DataSeed")] 14 | partial class DataSeed 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder.HasAnnotation("ProductVersion", "6.0.0"); 20 | 21 | modelBuilder.Entity("CityInfo.API.Entities.City", b => 22 | { 23 | b.Property("Id") 24 | .ValueGeneratedOnAdd() 25 | .HasColumnType("INTEGER"); 26 | 27 | b.Property("Description") 28 | .HasMaxLength(200) 29 | .HasColumnType("TEXT"); 30 | 31 | b.Property("Name") 32 | .IsRequired() 33 | .HasMaxLength(50) 34 | .HasColumnType("TEXT"); 35 | 36 | b.HasKey("Id"); 37 | 38 | b.ToTable("Cities"); 39 | 40 | b.HasData( 41 | new 42 | { 43 | Id = 1, 44 | Description = "The one with that big park.", 45 | Name = "New York City" 46 | }, 47 | new 48 | { 49 | Id = 2, 50 | Description = "The one with the cathedral that was never really finished.", 51 | Name = "Antwerp" 52 | }, 53 | new 54 | { 55 | Id = 3, 56 | Description = "The one with that big tower.", 57 | Name = "Paris" 58 | }); 59 | }); 60 | 61 | modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b => 62 | { 63 | b.Property("Id") 64 | .ValueGeneratedOnAdd() 65 | .HasColumnType("INTEGER"); 66 | 67 | b.Property("CityId") 68 | .HasColumnType("INTEGER"); 69 | 70 | b.Property("Description") 71 | .IsRequired() 72 | .HasMaxLength(200) 73 | .HasColumnType("TEXT"); 74 | 75 | b.Property("Name") 76 | .IsRequired() 77 | .HasMaxLength(50) 78 | .HasColumnType("TEXT"); 79 | 80 | b.HasKey("Id"); 81 | 82 | b.HasIndex("CityId"); 83 | 84 | b.ToTable("PointsOfInterest"); 85 | 86 | b.HasData( 87 | new 88 | { 89 | Id = 1, 90 | CityId = 1, 91 | Description = "The most visited urban park in the United States.", 92 | Name = "Central Park" 93 | }, 94 | new 95 | { 96 | Id = 2, 97 | CityId = 1, 98 | Description = "A 102-story skyscraper located in Midtown Manhattan.", 99 | Name = "Empire State Building" 100 | }, 101 | new 102 | { 103 | Id = 3, 104 | CityId = 2, 105 | Description = "A Gothic style cathedral, conceived by architects Jan and Pieter Appelmans.", 106 | Name = "Cathedral" 107 | }, 108 | new 109 | { 110 | Id = 4, 111 | CityId = 2, 112 | Description = "The the finest example of railway architecture in Belgium.", 113 | Name = "Antwerp Central Station" 114 | }, 115 | new 116 | { 117 | Id = 5, 118 | CityId = 3, 119 | Description = "A wrought iron lattice tower on the Champ de Mars, named after engineer Gustave Eiffel.", 120 | Name = "Eiffel Tower" 121 | }, 122 | new 123 | { 124 | Id = 6, 125 | CityId = 3, 126 | Description = "The world's largest museum.", 127 | Name = "The Louvre" 128 | }); 129 | }); 130 | 131 | modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b => 132 | { 133 | b.HasOne("CityInfo.API.Entities.City", "City") 134 | .WithMany("PointsOfInterest") 135 | .HasForeignKey("CityId") 136 | .OnDelete(DeleteBehavior.Cascade) 137 | .IsRequired(); 138 | 139 | b.Navigation("City"); 140 | }); 141 | 142 | modelBuilder.Entity("CityInfo.API.Entities.City", b => 143 | { 144 | b.Navigation("PointsOfInterest"); 145 | }); 146 | #pragma warning restore 612, 618 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CityInfo.API/Controllers/PointsOfInterestController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using CityInfo.API.Models; 3 | using CityInfo.API.Services; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.JsonPatch; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | namespace CityInfo.API.Controllers 10 | { 11 | [Route("api/v{version:apiVersion}/cities/{cityId}/pointsofinterest")] 12 | [Authorize(Policy = "MustBeFromAntwerp")] 13 | [ApiVersion("2.0")] 14 | [ApiController] 15 | public class PointsOfInterestController : ControllerBase 16 | { 17 | private readonly ILogger _logger; 18 | private readonly IMailService _mailService; 19 | private readonly ICityInfoRepository _cityInfoRepository; 20 | private readonly IMapper _mapper; 21 | 22 | public PointsOfInterestController(ILogger logger, 23 | IMailService mailService, 24 | ICityInfoRepository cityInfoRepository, 25 | IMapper mapper) 26 | { 27 | _logger = logger ?? 28 | throw new ArgumentNullException(nameof(logger)); 29 | _mailService = mailService ?? 30 | throw new ArgumentNullException(nameof(mailService)); 31 | _cityInfoRepository = cityInfoRepository ?? 32 | throw new ArgumentNullException(nameof(cityInfoRepository)); 33 | _mapper = mapper ?? 34 | throw new ArgumentNullException(nameof(mapper)); 35 | } 36 | 37 | [HttpGet] 38 | public async Task>> GetPointsOfInterest( 39 | int cityId) 40 | { 41 | 42 | //var cityName = User.Claims.FirstOrDefault(c => c.Type == "city")?.Value; 43 | 44 | //if (!await _cityInfoRepository.CityNameMatchesCityId(cityName, cityId)) 45 | //{ 46 | // return Forbid(); 47 | //} 48 | 49 | if (!await _cityInfoRepository.CityExistsAsync(cityId)) 50 | { 51 | _logger.LogInformation( 52 | $"City with id {cityId} wasn't found when accessing points of interest."); 53 | return NotFound(); 54 | } 55 | 56 | var pointsOfInterestForCity = await _cityInfoRepository 57 | .GetPointsOfInterestForCityAsync(cityId); 58 | 59 | return Ok(_mapper.Map>(pointsOfInterestForCity)); 60 | } 61 | 62 | [HttpGet("{pointofinterestid}", Name = "GetPointOfInterest")] 63 | public async Task> GetPointOfInterest( 64 | int cityId, int pointOfInterestId) 65 | { 66 | if (!await _cityInfoRepository.CityExistsAsync(cityId)) 67 | { 68 | return NotFound(); 69 | } 70 | 71 | var pointOfInterest = await _cityInfoRepository 72 | .GetPointOfInterestForCityAsync(cityId, pointOfInterestId); 73 | 74 | if (pointOfInterest == null) 75 | { 76 | return NotFound(); 77 | } 78 | 79 | return Ok(_mapper.Map(pointOfInterest)); 80 | } 81 | 82 | [HttpPost] 83 | public async Task> CreatePointOfInterest( 84 | int cityId, 85 | PointOfInterestForCreationDto pointOfInterest) 86 | { 87 | if (!await _cityInfoRepository.CityExistsAsync(cityId)) 88 | { 89 | return NotFound(); 90 | } 91 | 92 | var finalPointOfInterest = _mapper.Map(pointOfInterest); 93 | 94 | await _cityInfoRepository.AddPointOfInterestForCityAsync( 95 | cityId, finalPointOfInterest); 96 | 97 | await _cityInfoRepository.SaveChangesAsync(); 98 | 99 | var createdPointOfInterestToReturn = 100 | _mapper.Map(finalPointOfInterest); 101 | 102 | return CreatedAtRoute("GetPointOfInterest", 103 | new 104 | { 105 | cityId = cityId, 106 | pointOfInterestId = createdPointOfInterestToReturn.Id 107 | }, 108 | createdPointOfInterestToReturn); 109 | } 110 | 111 | [HttpPut("{pointofinterestid}")] 112 | public async Task UpdatePointOfInterest(int cityId, int pointOfInterestId, 113 | PointOfInterestForUpdateDto pointOfInterest) 114 | { 115 | if (!await _cityInfoRepository.CityExistsAsync(cityId)) 116 | { 117 | return NotFound(); 118 | } 119 | 120 | var pointOfInterestEntity = await _cityInfoRepository 121 | .GetPointOfInterestForCityAsync(cityId, pointOfInterestId); 122 | if (pointOfInterestEntity == null) 123 | { 124 | return NotFound(); 125 | } 126 | 127 | _mapper.Map(pointOfInterest, pointOfInterestEntity); 128 | 129 | await _cityInfoRepository.SaveChangesAsync(); 130 | 131 | return NoContent(); 132 | } 133 | 134 | 135 | [HttpPatch("{pointofinterestid}")] 136 | public async Task PartiallyUpdatePointOfInterest( 137 | int cityId, int pointOfInterestId, 138 | JsonPatchDocument patchDocument) 139 | { 140 | if (!await _cityInfoRepository.CityExistsAsync(cityId)) 141 | { 142 | return NotFound(); 143 | } 144 | 145 | var pointOfInterestEntity = await _cityInfoRepository 146 | .GetPointOfInterestForCityAsync(cityId, pointOfInterestId); 147 | if (pointOfInterestEntity == null) 148 | { 149 | return NotFound(); 150 | } 151 | 152 | var pointOfInterestToPatch = _mapper.Map( 153 | pointOfInterestEntity); 154 | 155 | patchDocument.ApplyTo(pointOfInterestToPatch, ModelState); 156 | 157 | if (!ModelState.IsValid) 158 | { 159 | return BadRequest(ModelState); 160 | } 161 | 162 | if (!TryValidateModel(pointOfInterestToPatch)) 163 | { 164 | return BadRequest(ModelState); 165 | } 166 | 167 | _mapper.Map(pointOfInterestToPatch, pointOfInterestEntity); 168 | await _cityInfoRepository.SaveChangesAsync(); 169 | 170 | return NoContent(); 171 | } 172 | 173 | [HttpDelete("{pointOfInterestId}")] 174 | public async Task DeletePointOfInterest( 175 | int cityId, int pointOfInterestId) 176 | { 177 | if (!await _cityInfoRepository.CityExistsAsync(cityId)) 178 | { 179 | return NotFound(); 180 | } 181 | 182 | var pointOfInterestEntity = await _cityInfoRepository 183 | .GetPointOfInterestForCityAsync(cityId, pointOfInterestId); 184 | if (pointOfInterestEntity == null) 185 | { 186 | return NotFound(); 187 | } 188 | 189 | _cityInfoRepository.DeletePointOfInterest(pointOfInterestEntity); 190 | await _cityInfoRepository.SaveChangesAsync(); 191 | 192 | _mailService.Send( 193 | "Point of interest deleted.", 194 | $"Point of interest {pointOfInterestEntity.Name} with id {pointOfInterestEntity.Id} was deleted."); 195 | 196 | return NoContent(); 197 | } 198 | 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /ASP.NET Core 6 Web API Fundamentals.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "6d3b2bfd-85f3-41e6-8a97-71cf04c5d421", 4 | "name": "ASP.NET Core 6 Web API Fundamentals", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "GET Cities", 10 | "request": { 11 | "method": "GET", 12 | "header": [], 13 | "url": { 14 | "raw": "https://localhost:{{portNumber}}/api/cities", 15 | "protocol": "https", 16 | "host": [ 17 | "localhost" 18 | ], 19 | "port": "{{portNumber}}", 20 | "path": [ 21 | "api", 22 | "cities" 23 | ] 24 | } 25 | }, 26 | "response": [] 27 | }, 28 | { 29 | "name": "GET City", 30 | "request": { 31 | "method": "GET", 32 | "header": [], 33 | "url": { 34 | "raw": "https://localhost:{{portNumber}}/api/cities/1", 35 | "protocol": "https", 36 | "host": [ 37 | "localhost" 38 | ], 39 | "port": "{{portNumber}}", 40 | "path": [ 41 | "api", 42 | "cities", 43 | "1" 44 | ] 45 | } 46 | }, 47 | "response": [] 48 | }, 49 | { 50 | "name": "GET Points of Interest", 51 | "request": { 52 | "method": "GET", 53 | "header": [], 54 | "url": { 55 | "raw": "https://localhost:{{portNumber}}/api/cities/1/pointsofinterest", 56 | "protocol": "https", 57 | "host": [ 58 | "localhost" 59 | ], 60 | "port": "{{portNumber}}", 61 | "path": [ 62 | "api", 63 | "cities", 64 | "1", 65 | "pointsofinterest" 66 | ] 67 | } 68 | }, 69 | "response": [] 70 | }, 71 | { 72 | "name": "GET Points of Interest (unexisting City)", 73 | "request": { 74 | "method": "GET", 75 | "header": [], 76 | "url": { 77 | "raw": "https://localhost:{{portNumber}}/api/cities/4/pointsofinterest", 78 | "protocol": "https", 79 | "host": [ 80 | "localhost" 81 | ], 82 | "port": "{{portNumber}}", 83 | "path": [ 84 | "api", 85 | "cities", 86 | "4", 87 | "pointsofinterest" 88 | ] 89 | }, 90 | "description": "Should return 404 NotFound" 91 | }, 92 | "response": [] 93 | }, 94 | { 95 | "name": "GET Point of Interest", 96 | "request": { 97 | "method": "GET", 98 | "header": [], 99 | "url": { 100 | "raw": "https://localhost:{{portNumber}}/api/cities/1/pointsofinterest/1", 101 | "protocol": "https", 102 | "host": [ 103 | "localhost" 104 | ], 105 | "port": "{{portNumber}}", 106 | "path": [ 107 | "api", 108 | "cities", 109 | "1", 110 | "pointsofinterest", 111 | "1" 112 | ] 113 | } 114 | }, 115 | "response": [] 116 | }, 117 | { 118 | "name": "GET Point of Interest (unexisting City)", 119 | "request": { 120 | "method": "GET", 121 | "header": [], 122 | "url": { 123 | "raw": "https://localhost:{{portNumber}}/api/cities/4/pointsofinterest/1", 124 | "protocol": "https", 125 | "host": [ 126 | "localhost" 127 | ], 128 | "port": "{{portNumber}}", 129 | "path": [ 130 | "api", 131 | "cities", 132 | "4", 133 | "pointsofinterest", 134 | "1" 135 | ] 136 | }, 137 | "description": "Should return 404 NotFound" 138 | }, 139 | "response": [] 140 | }, 141 | { 142 | "name": "GET Point of Interest (unexisting Point of Interest)", 143 | "request": { 144 | "method": "GET", 145 | "header": [], 146 | "url": { 147 | "raw": "https://localhost:{{portNumber}}/api/cities/1/pointsofinterest/10", 148 | "protocol": "https", 149 | "host": [ 150 | "localhost" 151 | ], 152 | "port": "{{portNumber}}", 153 | "path": [ 154 | "api", 155 | "cities", 156 | "1", 157 | "pointsofinterest", 158 | "10" 159 | ] 160 | }, 161 | "description": "Should return 404 NotFound" 162 | }, 163 | "response": [] 164 | }, 165 | { 166 | "name": "GET Cities (XML)", 167 | "request": { 168 | "method": "GET", 169 | "header": [ 170 | { 171 | "key": "Accept", 172 | "value": "application/xml" 173 | } 174 | ], 175 | "url": { 176 | "raw": "https://localhost:{{portNumber}}/api/cities", 177 | "protocol": "https", 178 | "host": [ 179 | "localhost" 180 | ], 181 | "port": "{{portNumber}}", 182 | "path": [ 183 | "api", 184 | "cities" 185 | ] 186 | } 187 | }, 188 | "response": [] 189 | }, 190 | { 191 | "name": "GET File", 192 | "request": { 193 | "method": "GET", 194 | "header": [ 195 | { 196 | "key": "Accept", 197 | "value": "application/xml" 198 | } 199 | ], 200 | "url": { 201 | "raw": "https://localhost:{{portNumber}}/api/files/1", 202 | "protocol": "https", 203 | "host": [ 204 | "localhost" 205 | ], 206 | "port": "{{portNumber}}", 207 | "path": [ 208 | "api", 209 | "files", 210 | "1" 211 | ] 212 | } 213 | }, 214 | "response": [] 215 | }, 216 | { 217 | "name": "POST Point of Interest", 218 | "request": { 219 | "method": "POST", 220 | "header": [ 221 | { 222 | "key": "Content-Type", 223 | "value": "application/json" 224 | } 225 | ], 226 | "body": { 227 | "mode": "raw", 228 | "raw": "{\n \"name\": \"Père Lachaise\",\n \"description\": \"Famous cemetery where Jim Morrison and Oscar Wilde are buried.\"\n}" 229 | }, 230 | "url": { 231 | "raw": "https://localhost:{{portNumber}}/api/cities/3/pointsofinterest", 232 | "protocol": "https", 233 | "host": [ 234 | "localhost" 235 | ], 236 | "port": "{{portNumber}}", 237 | "path": [ 238 | "api", 239 | "cities", 240 | "3", 241 | "pointsofinterest" 242 | ] 243 | } 244 | }, 245 | "response": [] 246 | }, 247 | { 248 | "name": "POST Point of Interest (can't deserialize)", 249 | "request": { 250 | "method": "POST", 251 | "header": [ 252 | { 253 | "key": "Content-Type", 254 | "value": "application/json" 255 | } 256 | ], 257 | "body": { 258 | "mode": "raw", 259 | "raw": "" 260 | }, 261 | "url": { 262 | "raw": "https://localhost:{{portNumber}}/api/cities/3/pointsofinterest", 263 | "protocol": "https", 264 | "host": [ 265 | "localhost" 266 | ], 267 | "port": "{{portNumber}}", 268 | "path": [ 269 | "api", 270 | "cities", 271 | "3", 272 | "pointsofinterest" 273 | ] 274 | } 275 | }, 276 | "response": [] 277 | }, 278 | { 279 | "name": "POST Point of Interest (missing name, long description)", 280 | "request": { 281 | "method": "POST", 282 | "header": [ 283 | { 284 | "key": "Content-Type", 285 | "value": "application/json" 286 | } 287 | ], 288 | "body": { 289 | "mode": "raw", 290 | "raw": "{\n \"invalidProperty\": 1,\n \"description\": \"Scallywag holystone landlubber or just lubber yardarm tackle Shiver me timbers cog heave down provost Admiral of the Black. Hornswaggle spanker man-of-war yo-ho-ho mutiny splice the main brace jack keelhaul fire ship Corsair. Bounty prow walk the plank lugsail port loot pirate bilge jib scuppers. Sutler lee matey sloop plunder splice the main brace interloper Yellow Jack maroon quarter. Draft Privateer run a shot across the bow chandler gaff broadside Pirate Round jolly boat skysail bilge. Chandler mutiny careen execution dock splice the main brace bring a spring upon her cable lass run a rig grog blossom smartly. Gangplank Davy Jones' Locker plunder overhaul draught pinnace blow the man down bring a spring upon her cable no prey, no pay keel. Gold Road gaff grapple sutler scurvy aft bilge come about coffer gunwalls. Scuttle list Davy Jones' Locker pinnace chase trysail draught Pirate Round Jolly Roger log.\"\n}\n" 291 | }, 292 | "url": { 293 | "raw": "https://localhost:{{portNumber}}/api/cities/3/pointsofinterest", 294 | "protocol": "https", 295 | "host": [ 296 | "localhost" 297 | ], 298 | "port": "{{portNumber}}", 299 | "path": [ 300 | "api", 301 | "cities", 302 | "3", 303 | "pointsofinterest" 304 | ] 305 | } 306 | }, 307 | "response": [] 308 | }, 309 | { 310 | "name": "PUT Point of Interest", 311 | "request": { 312 | "method": "PUT", 313 | "header": [ 314 | { 315 | "key": "Content-Type", 316 | "value": "application/json" 317 | } 318 | ], 319 | "body": { 320 | "mode": "raw", 321 | "raw": "{\n \"name\": \"Updated - Central Park\",\n \"description\": \"Updated - The most visited urban park in the United States.\"\n}" 322 | }, 323 | "url": { 324 | "raw": "https://localhost:{{portNumber}}/api/cities/1/pointsofinterest/1", 325 | "protocol": "https", 326 | "host": [ 327 | "localhost" 328 | ], 329 | "port": "{{portNumber}}", 330 | "path": [ 331 | "api", 332 | "cities", 333 | "1", 334 | "pointsofinterest", 335 | "1" 336 | ] 337 | } 338 | }, 339 | "response": [] 340 | }, 341 | { 342 | "name": "PUT Point of Interest (no description)", 343 | "request": { 344 | "method": "PUT", 345 | "header": [ 346 | { 347 | "key": "Content-Type", 348 | "value": "application/json" 349 | } 350 | ], 351 | "body": { 352 | "mode": "raw", 353 | "raw": "{\n \"name\": \"Updated again - Central Park\"\n}" 354 | }, 355 | "url": { 356 | "raw": "https://localhost:{{portNumber}}/api/cities/1/pointsofinterest/1", 357 | "protocol": "https", 358 | "host": [ 359 | "localhost" 360 | ], 361 | "port": "{{portNumber}}", 362 | "path": [ 363 | "api", 364 | "cities", 365 | "1", 366 | "pointsofinterest", 367 | "1" 368 | ] 369 | } 370 | }, 371 | "response": [] 372 | }, 373 | { 374 | "name": "PATCH Point of Interest", 375 | "request": { 376 | "method": "PATCH", 377 | "header": [ 378 | { 379 | "key": "Content-Type", 380 | "value": "application/json" 381 | } 382 | ], 383 | "body": { 384 | "mode": "raw", 385 | "raw": "[\n {\n \"op\": \"replace\",\n \"path\": \"/name\",\n \"value\": \"Updated - Central Park\"\n }\n]" 386 | }, 387 | "url": { 388 | "raw": "https://localhost:{{portNumber}}/api/cities/1/pointsofinterest/1", 389 | "protocol": "https", 390 | "host": [ 391 | "localhost" 392 | ], 393 | "port": "{{portNumber}}", 394 | "path": [ 395 | "api", 396 | "cities", 397 | "1", 398 | "pointsofinterest", 399 | "1" 400 | ] 401 | } 402 | }, 403 | "response": [] 404 | }, 405 | { 406 | "name": "PATCH Point of Interest (update multiple)", 407 | "request": { 408 | "method": "PATCH", 409 | "header": [ 410 | { 411 | "key": "Content-Type", 412 | "value": "application/json" 413 | } 414 | ], 415 | "body": { 416 | "mode": "raw", 417 | "raw": "[\n {\n \"op\": \"replace\",\n \"path\": \"/name\",\n \"value\": \"Updated - Central Park\"\n },\n {\n \"op\": \"replace\",\n \"path\": \"/description\",\n \"value\": \"Updated - Description\"\n }\n]" 418 | }, 419 | "url": { 420 | "raw": "https://localhost:{{portNumber}}/api/cities/1/pointsofinterest/1", 421 | "protocol": "https", 422 | "host": [ 423 | "localhost" 424 | ], 425 | "port": "{{portNumber}}", 426 | "path": [ 427 | "api", 428 | "cities", 429 | "1", 430 | "pointsofinterest", 431 | "1" 432 | ] 433 | } 434 | }, 435 | "response": [] 436 | }, 437 | { 438 | "name": "PATCH Point of Interest (invalid property)", 439 | "request": { 440 | "method": "PATCH", 441 | "header": [ 442 | { 443 | "key": "Content-Type", 444 | "value": "application/json" 445 | } 446 | ], 447 | "body": { 448 | "mode": "raw", 449 | "raw": "[\n {\n \"op\": \"replace\",\n \"path\": \"/invalidproperty\",\n \"value\": \"Updated - Central Park\"\n }\n]" 450 | }, 451 | "url": { 452 | "raw": "https://localhost:{{portNumber}}/api/cities/1/pointsofinterest/1", 453 | "protocol": "https", 454 | "host": [ 455 | "localhost" 456 | ], 457 | "port": "{{portNumber}}", 458 | "path": [ 459 | "api", 460 | "cities", 461 | "1", 462 | "pointsofinterest", 463 | "1" 464 | ] 465 | } 466 | }, 467 | "response": [] 468 | }, 469 | { 470 | "name": "PATCH Point of Interest (remove name)", 471 | "request": { 472 | "method": "PATCH", 473 | "header": [ 474 | { 475 | "key": "Content-Type", 476 | "value": "application/json" 477 | } 478 | ], 479 | "body": { 480 | "mode": "raw", 481 | "raw": "[\n {\n \"op\": \"remove\",\n \"path\": \"/name\"\n }\n]" 482 | }, 483 | "url": { 484 | "raw": "https://localhost:{{portNumber}}/api/cities/1/pointsofinterest/1", 485 | "protocol": "https", 486 | "host": [ 487 | "localhost" 488 | ], 489 | "port": "{{portNumber}}", 490 | "path": [ 491 | "api", 492 | "cities", 493 | "1", 494 | "pointsofinterest", 495 | "1" 496 | ] 497 | } 498 | }, 499 | "response": [] 500 | }, 501 | { 502 | "name": "DELETE Point of Interest", 503 | "request": { 504 | "method": "DELETE", 505 | "header": [], 506 | "body": { 507 | "mode": "raw", 508 | "raw": "" 509 | }, 510 | "url": { 511 | "raw": "https://localhost:{{portNumber}}/api/cities/1/pointsofinterest/1", 512 | "protocol": "https", 513 | "host": [ 514 | "localhost" 515 | ], 516 | "port": "{{portNumber}}", 517 | "path": [ 518 | "api", 519 | "cities", 520 | "1", 521 | "pointsofinterest", 522 | "1" 523 | ] 524 | } 525 | }, 526 | "response": [] 527 | }, 528 | { 529 | "name": "GET Cities, filtered", 530 | "request": { 531 | "method": "GET", 532 | "header": [], 533 | "url": { 534 | "raw": "https://localhost:{{portNumber}}/api/cities?name=Antwerp", 535 | "protocol": "https", 536 | "host": [ 537 | "localhost" 538 | ], 539 | "port": "{{portNumber}}", 540 | "path": [ 541 | "api", 542 | "cities" 543 | ], 544 | "query": [ 545 | { 546 | "key": "name", 547 | "value": "Antwerp" 548 | } 549 | ] 550 | } 551 | }, 552 | "response": [] 553 | }, 554 | { 555 | "name": "GET Cities, searched", 556 | "request": { 557 | "method": "GET", 558 | "header": [], 559 | "url": { 560 | "raw": "https://localhost:{{portNumber}}/api/cities?searchQuery=the", 561 | "protocol": "https", 562 | "host": [ 563 | "localhost" 564 | ], 565 | "port": "{{portNumber}}", 566 | "path": [ 567 | "api", 568 | "cities" 569 | ], 570 | "query": [ 571 | { 572 | "key": "searchQuery", 573 | "value": "the" 574 | } 575 | ] 576 | } 577 | }, 578 | "response": [] 579 | }, 580 | { 581 | "name": "GET Cities, filtered, searched", 582 | "request": { 583 | "method": "GET", 584 | "header": [], 585 | "url": { 586 | "raw": "https://localhost:{{portNumber}}/api/cities?name=Antwerp&searchQuery=the", 587 | "protocol": "https", 588 | "host": [ 589 | "localhost" 590 | ], 591 | "port": "{{portNumber}}", 592 | "path": [ 593 | "api", 594 | "cities" 595 | ], 596 | "query": [ 597 | { 598 | "key": "name", 599 | "value": "Antwerp" 600 | }, 601 | { 602 | "key": "searchQuery", 603 | "value": "the" 604 | } 605 | ] 606 | } 607 | }, 608 | "response": [] 609 | }, 610 | { 611 | "name": "GET Cities, paged", 612 | "request": { 613 | "method": "GET", 614 | "header": [], 615 | "url": { 616 | "raw": "https://localhost:{{portNumber}}/api/cities?pageSize=1&pageNumber=2", 617 | "protocol": "https", 618 | "host": [ 619 | "localhost" 620 | ], 621 | "port": "{{portNumber}}", 622 | "path": [ 623 | "api", 624 | "cities" 625 | ], 626 | "query": [ 627 | { 628 | "key": "pageSize", 629 | "value": "1" 630 | }, 631 | { 632 | "key": "pageNumber", 633 | "value": "2" 634 | } 635 | ] 636 | } 637 | }, 638 | "response": [] 639 | }, 640 | { 641 | "name": "POST Authentication info to get a token", 642 | "request": { 643 | "method": "POST", 644 | "header": [], 645 | "body": { 646 | "mode": "raw", 647 | "raw": "{\r\n \"username\": \"KevinDockx\",\r\n \"password\": \"This is a relatively long sentence that acts as my password\"\r\n}", 648 | "options": { 649 | "raw": { 650 | "language": "json" 651 | } 652 | } 653 | }, 654 | "url": { 655 | "raw": "https://localhost:{{portNumber}}/api/authentication/authenticate", 656 | "protocol": "https", 657 | "host": [ 658 | "localhost" 659 | ], 660 | "port": "{{portNumber}}", 661 | "path": [ 662 | "api", 663 | "authentication", 664 | "authenticate" 665 | ] 666 | } 667 | }, 668 | "response": [] 669 | } 670 | ] 671 | } --------------------------------------------------------------------------------