├── .gitignore ├── LICENSE ├── Movies.API ├── Contexts │ └── MoviesContext.cs ├── Controllers │ ├── MoviesController.cs │ ├── PostersController.cs │ └── TrailersController.cs ├── Entities │ ├── Director.cs │ └── Movie.cs ├── InternalModels │ ├── Poster.cs │ └── Trailer.cs ├── Migrations │ ├── InitialMigration.Designer.cs │ ├── InitialMigration.cs │ └── MoviesContextModelSnapshot.cs ├── Models │ ├── Movie.cs │ ├── MovieForCreation.cs │ ├── MovieForUpdate.cs │ ├── Poster.cs │ ├── PosterForCreation.cs │ ├── Trailer.cs │ └── TrailerForCreation.cs ├── Movies.API.csproj ├── Profiles │ ├── MoviesProfile.cs │ ├── PostersProfile.cs │ └── TrailersProfile.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Services │ ├── IMoviesRepository.cs │ ├── IPostersRepository.cs │ ├── ITrailersRepository.cs │ ├── MoviesRepository.cs │ ├── PostersRepository.cs │ └── TrailersRepository.cs ├── Startup.cs ├── appsettings.Development.json └── appsettings.json ├── Movies.Client ├── Models │ └── GeneratedDtoModels.cs ├── Movies.Client.csproj ├── Program.cs ├── Services │ ├── CancellationService.cs │ ├── CompressionService.cs │ ├── HttpClientFactoryInstanceManagementService.cs │ ├── HttpHandlersService.cs │ ├── IIntegrationService.cs │ └── StreamService.cs └── TimeOutDelegatingHandler.cs ├── Movies.sln └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This .gitignore file was automatically created by Microsoft(R) Visual Studio. 3 | ################################################################################ 4 | 5 | /.vs 6 | /Finished sample/Marvin.IDP/obj 7 | /Finished sample/Movies.API/bin/Debug/netcoreapp2.1 8 | /Finished sample/Movies.API/obj 9 | /Finished sample/Movies.Client/bin/Debug 10 | /Finished sample/Movies.Client/obj 11 | /Finished sample/Movies.Test/bin/Debug/netcoreapp2.1 12 | /Finished sample/Movies.Test/obj 13 | /Starter files/.vs/Movies/v15 14 | /Starter files/Movies.API/bin/Debug/netcoreapp2.1 15 | /Starter files/Movies.API/obj 16 | /Starter files/Movies.Client/bin/Debug 17 | /Starter files/Movies.Client/obj 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kevin Dockx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Movies.API/Contexts/MoviesContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Movies.API.Entities; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace Movies.API.Contexts 9 | { 10 | public class MoviesContext : DbContext 11 | { 12 | public DbSet Movies { get; set; } 13 | 14 | public MoviesContext(DbContextOptions options) 15 | : base(options) 16 | { 17 | } 18 | 19 | // seed the database with data 20 | protected override void OnModelCreating(ModelBuilder modelBuilder) 21 | { 22 | modelBuilder.Entity().HasData( 23 | new Director() 24 | { 25 | Id = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 26 | FirstName = "Quentin", 27 | LastName = "Tarantino" 28 | }, 29 | new Director() 30 | { 31 | Id = Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 32 | FirstName = "Joel", 33 | LastName = "Coen" 34 | }, 35 | new Director() 36 | { 37 | Id = Guid.Parse("c19099ed-94db-44ba-885b-0ad7205d5e40"), 38 | FirstName = "Martin", 39 | LastName = "Scorsese" 40 | }, 41 | new Director() 42 | { 43 | Id = Guid.Parse("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), 44 | FirstName = "David", 45 | LastName = "Fincher" 46 | }, 47 | new Director() 48 | { 49 | Id = Guid.Parse("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), 50 | FirstName = "Bryan", 51 | LastName = "Singer" 52 | }, 53 | new Director() 54 | { 55 | Id = Guid.Parse("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), 56 | FirstName = "James", 57 | LastName = "Cameron" 58 | }); 59 | 60 | modelBuilder.Entity().HasData( 61 | new Movie 62 | { 63 | Id = Guid.Parse("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 64 | DirectorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 65 | Title = "Pulp Fiction", 66 | Description = "The lives of two mob hitmen, a boxer, a gangster's wife, and a pair of diner bandits intertwine in four tales of violence and redemption.", 67 | ReleaseDate = new DateTimeOffset(new DateTime(1994,11,9)), 68 | Genre = "Crime, Drama" 69 | }, 70 | new Movie 71 | { 72 | Id = Guid.Parse("6e87f657-f2c1-4d90-9b37-cbe43cc6adb9"), 73 | DirectorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 74 | Title = "Jackie Brown", 75 | Description = "A middle-aged woman finds herself in the middle of a huge conflict that will either make her a profit or cost her life.", 76 | ReleaseDate = new DateTimeOffset(new DateTime(1997, 12, 25)), 77 | Genre = "Crime, Drama" 78 | }, 79 | new Movie 80 | { 81 | Id = Guid.Parse("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 82 | DirectorId = Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 83 | Title = "The Big Lebowski", 84 | Description = "The Dude (Lebowski), mistaken for a millionaire Lebowski, seeks restitution for his ruined rug and enlists his bowling buddies to help get it.", 85 | ReleaseDate = new DateTimeOffset(new DateTime(1998, 3, 6)), 86 | Genre = "Comedy, Crime" 87 | }, 88 | new Movie 89 | { 90 | Id = Guid.Parse("f9a16fee-4c49-41bb-87a1-bbaad0cd1174"), 91 | DirectorId = Guid.Parse("c19099ed-94db-44ba-885b-0ad7205d5e40"), 92 | Title = "Casino", 93 | Description = "A tale of greed, deception, money, power, and murder occur between two best friends: a mafia enforcer and a casino executive, compete against each other over a gambling empire, and over a fast living and fast loving socialite.", 94 | ReleaseDate = new DateTimeOffset(new DateTime(1995, 11, 22)), 95 | Genre = "Crime, Drama" 96 | }, 97 | new Movie 98 | { 99 | Id = Guid.Parse("bb6a100a-053f-4bf8-b271-60ce3aae6eb5"), 100 | DirectorId = Guid.Parse("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), 101 | Title = "Fight Club", 102 | Description = "An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.", 103 | ReleaseDate = new DateTimeOffset(new DateTime(1999, 10, 15)), 104 | Genre = "Drama" 105 | }, 106 | new Movie 107 | { 108 | Id = Guid.Parse("3d2880ae-5ba6-417c-845d-f4ebfd4bcac7"), 109 | DirectorId = Guid.Parse("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), 110 | Title = "The Usual Suspects", 111 | Description = "A sole survivor tells of the twisty events leading up to a horrific gun battle on a boat, which began when five criminals met at a seemingly random police lineup.", 112 | ReleaseDate = new DateTimeOffset(new DateTime(1995, 9, 15)), 113 | Genre = "Crime, Thriller" 114 | }, 115 | new Movie 116 | { 117 | Id = Guid.Parse("26fcbcc4-b7f7-47fc-9382-740c12246b59"), 118 | DirectorId = Guid.Parse("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), 119 | Title = "Terminator 2: Judgment Day", 120 | Description = "A cyborg, identical to the one who failed to kill Sarah Connor, must now protect her teenage son, John Connor, from a more advanced and powerful cyborg.", 121 | ReleaseDate = new DateTimeOffset(new DateTime(1991, 7, 3)), 122 | Genre = "Action, Sci-Fi" 123 | }); 124 | 125 | base.OnModelCreating(modelBuilder); 126 | } 127 | 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Movies.API/Controllers/MoviesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using AutoMapper; 6 | using Microsoft.AspNetCore.JsonPatch; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Movies.API.Entities; 9 | using Movies.API.Services; 10 | 11 | namespace Movies.API.Controllers 12 | { 13 | [Route("api/movies")] 14 | [ApiController] 15 | public class MoviesController : ControllerBase 16 | { 17 | private readonly IMoviesRepository _moviesRepository; 18 | private readonly IMapper _mapper; 19 | 20 | public MoviesController(IMoviesRepository moviesRepository, 21 | IMapper mapper) 22 | { 23 | _moviesRepository = moviesRepository ?? throw new ArgumentNullException(nameof(moviesRepository)); 24 | _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 25 | } 26 | 27 | [HttpGet] 28 | public async Task>> GetMovies() 29 | { 30 | var movieEntities = await _moviesRepository.GetMoviesAsync(); 31 | return Ok(_mapper.Map>(movieEntities)); 32 | } 33 | 34 | 35 | [HttpGet("{movieId}", Name = "GetMovie")] 36 | public async Task> GetMovie(Guid movieId) 37 | { 38 | var movieEntity = await _moviesRepository.GetMovieAsync(movieId); 39 | if (movieEntity == null) 40 | { 41 | return NotFound(); 42 | } 43 | 44 | return Ok(_mapper.Map(movieEntity)); 45 | } 46 | 47 | [HttpPost] 48 | public async Task CreateMovie( 49 | [FromBody] Models.MovieForCreation movieForCreation) 50 | { 51 | // model validation 52 | if (movieForCreation == null) 53 | { 54 | return BadRequest(); 55 | } 56 | 57 | if (!ModelState.IsValid) 58 | { 59 | // return 422 - Unprocessable Entity when validation fails 60 | return new UnprocessableEntityObjectResult(ModelState); 61 | } 62 | 63 | var movieEntity = _mapper.Map(movieForCreation); 64 | _moviesRepository.AddMovie(movieEntity); 65 | 66 | // save the changes 67 | await _moviesRepository.SaveChangesAsync(); 68 | 69 | // Fetch the movie from the data store so the director is included 70 | await _moviesRepository.GetMovieAsync(movieEntity.Id); 71 | 72 | return CreatedAtRoute("GetMovie", 73 | new { movieId = movieEntity.Id }, 74 | _mapper.Map(movieEntity)); 75 | } 76 | 77 | [HttpPut("{movieId}")] 78 | public async Task UpdateMovie(Guid movieId, 79 | [FromBody] Models.MovieForUpdate movieForUpdate) 80 | { 81 | // model validation 82 | if (movieForUpdate == null) 83 | { 84 | //return BadRequest(); 85 | } 86 | 87 | if (!ModelState.IsValid) 88 | { 89 | // return 422 - Unprocessable Entity when validation fails 90 | return new UnprocessableEntityObjectResult(ModelState); 91 | } 92 | 93 | var movieEntity = await _moviesRepository.GetMovieAsync(movieId); 94 | if (movieEntity == null) 95 | { 96 | return NotFound(); 97 | } 98 | 99 | // map the inputted object into the movie entity 100 | // this ensures properties will get updated 101 | _mapper.Map(movieForUpdate, movieEntity); 102 | 103 | // call into UpdateMovie even though in our implementation 104 | // this doesn't contain code - doing this ensures the code stays 105 | // reliable when other repository implemenations (eg: a mock 106 | // repository) are used. 107 | _moviesRepository.UpdateMovie(movieEntity); 108 | 109 | await _moviesRepository.SaveChangesAsync(); 110 | 111 | // return the updated movie, after mapping it 112 | return Ok(_mapper.Map(movieEntity)); 113 | } 114 | 115 | [HttpPatch("{movieId}")] 116 | public async Task PartiallyUpdateMovie(Guid movieId, 117 | [FromBody] JsonPatchDocument patchDoc) 118 | { 119 | var movieEntity = await _moviesRepository.GetMovieAsync(movieId); 120 | if (movieEntity == null) 121 | { 122 | return NotFound(); 123 | } 124 | 125 | // the patch is on a DTO, not on the movie entity 126 | var movieToPatch = Mapper.Map(movieEntity); 127 | 128 | patchDoc.ApplyTo(movieToPatch, ModelState); 129 | 130 | if (!ModelState.IsValid) 131 | { 132 | return new UnprocessableEntityObjectResult(ModelState); 133 | } 134 | 135 | // map back to the entity, and save 136 | Mapper.Map(movieToPatch, movieEntity); 137 | 138 | // call into UpdateMovie even though in our implementation 139 | // this doesn't contain code - doing this ensures the code stays 140 | // reliable when other repository implemenations (eg: a mock 141 | // repository) are used. 142 | _moviesRepository.UpdateMovie(movieEntity); 143 | 144 | await _moviesRepository.SaveChangesAsync(); 145 | 146 | // return the updated movie, after mapping it 147 | return Ok(_mapper.Map(movieEntity)); 148 | } 149 | 150 | [HttpDelete("{movieid}")] 151 | public async Task DeleteMovie(Guid movieId) 152 | { 153 | var movieEntity = await _moviesRepository.GetMovieAsync(movieId); 154 | if (movieEntity == null) 155 | { 156 | return NotFound(); 157 | } 158 | 159 | _moviesRepository.DeleteMovie(movieEntity); 160 | await _moviesRepository.SaveChangesAsync(); 161 | 162 | return NoContent(); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /Movies.API/Controllers/PostersController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Movies.API.InternalModels; 4 | using Movies.API.Services; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace Movies.API.Controllers 11 | { 12 | [Route("api/movies/{movieId}/posters")] 13 | [ApiController] 14 | public class PostersController : ControllerBase 15 | { 16 | private readonly IPostersRepository _postersRepository; 17 | private readonly IMapper _mapper; 18 | 19 | public PostersController(IPostersRepository postersRepository, 20 | IMapper mapper) 21 | { 22 | _postersRepository = postersRepository ?? throw new ArgumentNullException(nameof(postersRepository)); 23 | _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 24 | } 25 | 26 | [HttpGet("{posterId}", Name = "GetPoster")] 27 | public async Task> GetPoster(Guid movieId, Guid posterId) 28 | { 29 | var poster = await _postersRepository.GetPosterAsync(movieId, posterId); 30 | if (poster == null) 31 | { 32 | return NotFound(); 33 | } 34 | 35 | return Ok(_mapper.Map(poster)); 36 | } 37 | 38 | [HttpPost] 39 | public async Task CreatePoster(Guid movieId, 40 | [FromBody] Models.PosterForCreation posterForCreation) 41 | { 42 | // model validation 43 | if (posterForCreation == null) 44 | { 45 | return BadRequest(); 46 | } 47 | 48 | if (!ModelState.IsValid) 49 | { 50 | // return 422 - Unprocessable Entity when validation fails 51 | return new UnprocessableEntityObjectResult(ModelState); 52 | } 53 | 54 | var poster = _mapper.Map(posterForCreation); 55 | var createdPoster = await _postersRepository.AddPoster(movieId, poster); 56 | 57 | // no need to save, in this type of repo the poster is 58 | // immediately persisted. 59 | 60 | // map the poster from the repository to a shared model poster 61 | return CreatedAtRoute("GetPoster", 62 | new { movieId, posterId = createdPoster.Id }, 63 | _mapper.Map(createdPoster)); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Movies.API/Controllers/TrailersController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Movies.API.InternalModels; 4 | using Movies.API.Services; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace Movies.API.Controllers 11 | { 12 | [Route("api/movies/{movieId}/trailers")] 13 | [ApiController] 14 | public class TrailersController : ControllerBase 15 | { 16 | private readonly ITrailersRepository _trailersRepository; 17 | private readonly IMapper _mapper; 18 | 19 | public TrailersController(ITrailersRepository trailersRepository, 20 | IMapper mapper) 21 | { 22 | _trailersRepository = trailersRepository ?? throw new ArgumentNullException(nameof(trailersRepository)); 23 | _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 24 | } 25 | 26 | [HttpGet("{trailerId}", Name = "GetTrailer")] 27 | public async Task> GetTrailer(Guid movieId, Guid trailerId) 28 | { 29 | var trailer = await _trailersRepository.GetTrailerAsync(movieId, trailerId); 30 | if (trailer == null) 31 | { 32 | return NotFound(); 33 | } 34 | 35 | return Ok(_mapper.Map(trailer)); 36 | } 37 | 38 | [HttpPost] 39 | public async Task CreateTrailer(Guid movieId, 40 | [FromBody] Models.TrailerForCreation trailerForCreation) 41 | { 42 | // model validation 43 | if (trailerForCreation == null) 44 | { 45 | return BadRequest(); 46 | } 47 | 48 | if (!ModelState.IsValid) 49 | { 50 | // return 422 - Unprocessable Entity when validation fails 51 | return new UnprocessableEntityObjectResult(ModelState); 52 | } 53 | 54 | var trailer = _mapper.Map(trailerForCreation); 55 | var createdTrailer = await _trailersRepository.AddTrailer(movieId, trailer); 56 | 57 | // no need to save, in this type of repo the trailer is 58 | // immediately persisted. 59 | 60 | // map the trailer from the repository to a shared model trailer 61 | return CreatedAtRoute("GetTrailer", 62 | new { movieId, trailerId = createdTrailer.Id }, 63 | _mapper.Map(createdTrailer)); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Movies.API/Entities/Director.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace Movies.API.Entities 9 | { 10 | [Table("Directors")] 11 | public class Director 12 | { 13 | [Key] 14 | public Guid Id { get; set; } 15 | 16 | [Required] 17 | [MaxLength(200)] 18 | public string FirstName { get; set; } 19 | 20 | [Required] 21 | [MaxLength(200)] 22 | public string LastName { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Movies.API/Entities/Movie.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace Movies.API.Entities 9 | { 10 | [Table("Movies")] 11 | public class Movie 12 | { 13 | [Key] 14 | public Guid Id { get; set; } 15 | 16 | [Required] 17 | [MaxLength(200)] 18 | public string Title { get; set; } 19 | 20 | [MaxLength(2000)] 21 | public string Description { get; set; } 22 | 23 | [MaxLength(200)] 24 | public string Genre { get; set; } 25 | 26 | [Required] 27 | public DateTimeOffset ReleaseDate { get; set; } 28 | 29 | [Required] 30 | public Guid DirectorId { get; set; } 31 | public Director Director { get; set; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Movies.API/InternalModels/Poster.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Movies.API.InternalModels 8 | { 9 | public class Poster 10 | { 11 | [Required] 12 | public Guid Id { get; set; } 13 | 14 | [Required] 15 | public Guid MovieId { get; set; } 16 | 17 | [Required] 18 | [MaxLength(200)] 19 | public string Name { get; set; } 20 | 21 | [Required] 22 | public byte[] Bytes { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Movies.API/InternalModels/Trailer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Movies.API.InternalModels 8 | { 9 | public class Trailer 10 | { 11 | [Required] 12 | public Guid Id { get; set; } 13 | 14 | [Required] 15 | public Guid MovieId { get; set; } 16 | 17 | [Required] 18 | [MaxLength(200)] 19 | public string Name { get; set; } 20 | 21 | [MaxLength(1000)] 22 | public string Description { get; set; } 23 | 24 | [Required] 25 | public byte[] Bytes { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Movies.API/Migrations/InitialMigration.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using Movies.API.Contexts; 9 | 10 | namespace Movies.API.Migrations 11 | { 12 | [DbContext(typeof(MoviesContext))] 13 | [Migration("InitialMigration")] 14 | partial class InitialMigration 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.1.3-rtm-32065") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Movies.API.Entities.Director", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd(); 28 | 29 | b.Property("FirstName") 30 | .IsRequired() 31 | .HasMaxLength(200); 32 | 33 | b.Property("LastName") 34 | .IsRequired() 35 | .HasMaxLength(200); 36 | 37 | b.HasKey("Id"); 38 | 39 | b.ToTable("Directors"); 40 | 41 | b.HasData( 42 | new { Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), FirstName = "Quentin", LastName = "Tarantino" }, 43 | new { Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), FirstName = "Joel", LastName = "Coen" }, 44 | new { Id = new Guid("c19099ed-94db-44ba-885b-0ad7205d5e40"), FirstName = "Martin", LastName = "Scorsese" }, 45 | new { Id = new Guid("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), FirstName = "David", LastName = "Fincher" }, 46 | new { Id = new Guid("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), FirstName = "Bryan", LastName = "Singer" }, 47 | new { Id = new Guid("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), FirstName = "James", LastName = "Cameron" } 48 | ); 49 | }); 50 | 51 | modelBuilder.Entity("Movies.API.Entities.Movie", b => 52 | { 53 | b.Property("Id") 54 | .ValueGeneratedOnAdd(); 55 | 56 | b.Property("Description") 57 | .HasMaxLength(2000); 58 | 59 | b.Property("DirectorId"); 60 | 61 | b.Property("Genre") 62 | .HasMaxLength(200); 63 | 64 | b.Property("ReleaseDate"); 65 | 66 | b.Property("Title") 67 | .IsRequired() 68 | .HasMaxLength(200); 69 | 70 | b.HasKey("Id"); 71 | 72 | b.HasIndex("DirectorId"); 73 | 74 | b.ToTable("Movies"); 75 | 76 | b.HasData( 77 | new { Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), Description = "The lives of two mob hitmen, a boxer, a gangster's wife, and a pair of diner bandits intertwine in four tales of violence and redemption.", DirectorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), Genre = "Crime, Drama", ReleaseDate = new DateTimeOffset(new DateTime(1994, 11, 9, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), Title = "Pulp Fiction" }, 78 | new { Id = new Guid("6e87f657-f2c1-4d90-9b37-cbe43cc6adb9"), Description = "A middle-aged woman finds herself in the middle of a huge conflict that will either make her a profit or cost her life.", DirectorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), Genre = "Crime, Drama", ReleaseDate = new DateTimeOffset(new DateTime(1997, 12, 25, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), Title = "Jackie Brown" }, 79 | new { Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), Description = "The Dude (Lebowski), mistaken for a millionaire Lebowski, seeks restitution for his ruined rug and enlists his bowling buddies to help get it.", DirectorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), Genre = "Comedy, Crime", ReleaseDate = new DateTimeOffset(new DateTime(1998, 3, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), Title = "The Big Lebowski" }, 80 | new { Id = new Guid("f9a16fee-4c49-41bb-87a1-bbaad0cd1174"), Description = "A tale of greed, deception, money, power, and murder occur between two best friends: a mafia enforcer and a casino executive, compete against each other over a gambling empire, and over a fast living and fast loving socialite.", DirectorId = new Guid("c19099ed-94db-44ba-885b-0ad7205d5e40"), Genre = "Crime, Drama", ReleaseDate = new DateTimeOffset(new DateTime(1995, 11, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), Title = "Casino" }, 81 | new { Id = new Guid("bb6a100a-053f-4bf8-b271-60ce3aae6eb5"), Description = "An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.", DirectorId = new Guid("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), Genre = "Drama", ReleaseDate = new DateTimeOffset(new DateTime(1999, 10, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), Title = "Fight Club" }, 82 | new { Id = new Guid("3d2880ae-5ba6-417c-845d-f4ebfd4bcac7"), Description = "A sole survivor tells of the twisty events leading up to a horrific gun battle on a boat, which began when five criminals met at a seemingly random police lineup.", DirectorId = new Guid("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), Genre = "Crime, Thriller", ReleaseDate = new DateTimeOffset(new DateTime(1995, 9, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), Title = "The Usual Suspects" }, 83 | new { Id = new Guid("26fcbcc4-b7f7-47fc-9382-740c12246b59"), Description = "AA cyborg, identical to the one who failed to kill Sarah Connor, must now protect her teenage son, John Connor, from a more advanced and powerful cyborg.", DirectorId = new Guid("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), Genre = "Action, Sci-Fi", ReleaseDate = new DateTimeOffset(new DateTime(1991, 7, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), Title = "Terminator 2: Judgment Day" } 84 | ); 85 | }); 86 | 87 | modelBuilder.Entity("Movies.API.Entities.Movie", b => 88 | { 89 | b.HasOne("Movies.API.Entities.Director", "Director") 90 | .WithMany() 91 | .HasForeignKey("DirectorId") 92 | .OnDelete(DeleteBehavior.Cascade); 93 | }); 94 | #pragma warning restore 612, 618 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Movies.API/Migrations/InitialMigration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Movies.API.Migrations 5 | { 6 | public partial class InitialMigration : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "Directors", 12 | columns: table => new 13 | { 14 | Id = table.Column(nullable: false), 15 | FirstName = table.Column(maxLength: 200, nullable: false), 16 | LastName = table.Column(maxLength: 200, nullable: false) 17 | }, 18 | constraints: table => 19 | { 20 | table.PrimaryKey("PK_Directors", x => x.Id); 21 | }); 22 | 23 | migrationBuilder.CreateTable( 24 | name: "Movies", 25 | columns: table => new 26 | { 27 | Id = table.Column(nullable: false), 28 | Title = table.Column(maxLength: 200, nullable: false), 29 | Description = table.Column(maxLength: 2000, nullable: true), 30 | Genre = table.Column(maxLength: 200, nullable: true), 31 | ReleaseDate = table.Column(nullable: false), 32 | DirectorId = table.Column(nullable: false) 33 | }, 34 | constraints: table => 35 | { 36 | table.PrimaryKey("PK_Movies", x => x.Id); 37 | table.ForeignKey( 38 | name: "FK_Movies_Directors_DirectorId", 39 | column: x => x.DirectorId, 40 | principalTable: "Directors", 41 | principalColumn: "Id", 42 | onDelete: ReferentialAction.Cascade); 43 | }); 44 | 45 | migrationBuilder.InsertData( 46 | table: "Directors", 47 | columns: new[] { "Id", "FirstName", "LastName" }, 48 | values: new object[,] 49 | { 50 | { new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), "Quentin", "Tarantino" }, 51 | { new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), "Joel", "Coen" }, 52 | { new Guid("c19099ed-94db-44ba-885b-0ad7205d5e40"), "Martin", "Scorsese" }, 53 | { new Guid("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), "David", "Fincher" }, 54 | { new Guid("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), "Bryan", "Singer" }, 55 | { new Guid("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), "James", "Cameron" } 56 | }); 57 | 58 | migrationBuilder.InsertData( 59 | table: "Movies", 60 | columns: new[] { "Id", "Description", "DirectorId", "Genre", "ReleaseDate", "Title" }, 61 | values: new object[,] 62 | { 63 | { new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), "The lives of two mob hitmen, a boxer, a gangster's wife, and a pair of diner bandits intertwine in four tales of violence and redemption.", new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), "Crime, Drama", new DateTimeOffset(new DateTime(1994, 11, 9, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), "Pulp Fiction" }, 64 | { new Guid("6e87f657-f2c1-4d90-9b37-cbe43cc6adb9"), "A middle-aged woman finds herself in the middle of a huge conflict that will either make her a profit or cost her life.", new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), "Crime, Drama", new DateTimeOffset(new DateTime(1997, 12, 25, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), "Jackie Brown" }, 65 | { new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), "The Dude (Lebowski), mistaken for a millionaire Lebowski, seeks restitution for his ruined rug and enlists his bowling buddies to help get it.", new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), "Comedy, Crime", new DateTimeOffset(new DateTime(1998, 3, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), "The Big Lebowski" }, 66 | { new Guid("f9a16fee-4c49-41bb-87a1-bbaad0cd1174"), "A tale of greed, deception, money, power, and murder occur between two best friends: a mafia enforcer and a casino executive, compete against each other over a gambling empire, and over a fast living and fast loving socialite.", new Guid("c19099ed-94db-44ba-885b-0ad7205d5e40"), "Crime, Drama", new DateTimeOffset(new DateTime(1995, 11, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), "Casino" }, 67 | { new Guid("bb6a100a-053f-4bf8-b271-60ce3aae6eb5"), "An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.", new Guid("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), "Drama", new DateTimeOffset(new DateTime(1999, 10, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), "Fight Club" }, 68 | { new Guid("3d2880ae-5ba6-417c-845d-f4ebfd4bcac7"), "A sole survivor tells of the twisty events leading up to a horrific gun battle on a boat, which began when five criminals met at a seemingly random police lineup.", new Guid("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), "Crime, Thriller", new DateTimeOffset(new DateTime(1995, 9, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), "The Usual Suspects" }, 69 | { new Guid("26fcbcc4-b7f7-47fc-9382-740c12246b59"), "AA cyborg, identical to the one who failed to kill Sarah Connor, must now protect her teenage son, John Connor, from a more advanced and powerful cyborg.", new Guid("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), "Action, Sci-Fi", new DateTimeOffset(new DateTime(1991, 7, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), "Terminator 2: Judgment Day" } 70 | }); 71 | 72 | migrationBuilder.CreateIndex( 73 | name: "IX_Movies_DirectorId", 74 | table: "Movies", 75 | column: "DirectorId"); 76 | } 77 | 78 | protected override void Down(MigrationBuilder migrationBuilder) 79 | { 80 | migrationBuilder.DropTable( 81 | name: "Movies"); 82 | 83 | migrationBuilder.DropTable( 84 | name: "Directors"); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Movies.API/Migrations/MoviesContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Movies.API.Contexts; 8 | 9 | namespace Movies.API.Migrations 10 | { 11 | [DbContext(typeof(MoviesContext))] 12 | partial class MoviesContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "2.1.3-rtm-32065") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("Movies.API.Entities.Director", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd(); 26 | 27 | b.Property("FirstName") 28 | .IsRequired() 29 | .HasMaxLength(200); 30 | 31 | b.Property("LastName") 32 | .IsRequired() 33 | .HasMaxLength(200); 34 | 35 | b.HasKey("Id"); 36 | 37 | b.ToTable("Directors"); 38 | 39 | b.HasData( 40 | new { Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), FirstName = "Quentin", LastName = "Tarantino" }, 41 | new { Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), FirstName = "Joel", LastName = "Coen" }, 42 | new { Id = new Guid("c19099ed-94db-44ba-885b-0ad7205d5e40"), FirstName = "Martin", LastName = "Scorsese" }, 43 | new { Id = new Guid("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), FirstName = "David", LastName = "Fincher" }, 44 | new { Id = new Guid("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), FirstName = "Bryan", LastName = "Singer" }, 45 | new { Id = new Guid("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), FirstName = "James", LastName = "Cameron" } 46 | ); 47 | }); 48 | 49 | modelBuilder.Entity("Movies.API.Entities.Movie", b => 50 | { 51 | b.Property("Id") 52 | .ValueGeneratedOnAdd(); 53 | 54 | b.Property("Description") 55 | .HasMaxLength(2000); 56 | 57 | b.Property("DirectorId"); 58 | 59 | b.Property("Genre") 60 | .HasMaxLength(200); 61 | 62 | b.Property("ReleaseDate"); 63 | 64 | b.Property("Title") 65 | .IsRequired() 66 | .HasMaxLength(200); 67 | 68 | b.HasKey("Id"); 69 | 70 | b.HasIndex("DirectorId"); 71 | 72 | b.ToTable("Movies"); 73 | 74 | b.HasData( 75 | new { Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), Description = "The lives of two mob hitmen, a boxer, a gangster's wife, and a pair of diner bandits intertwine in four tales of violence and redemption.", DirectorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), Genre = "Crime, Drama", ReleaseDate = new DateTimeOffset(new DateTime(1994, 11, 9, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), Title = "Pulp Fiction" }, 76 | new { Id = new Guid("6e87f657-f2c1-4d90-9b37-cbe43cc6adb9"), Description = "A middle-aged woman finds herself in the middle of a huge conflict that will either make her a profit or cost her life.", DirectorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), Genre = "Crime, Drama", ReleaseDate = new DateTimeOffset(new DateTime(1997, 12, 25, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), Title = "Jackie Brown" }, 77 | new { Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), Description = "The Dude (Lebowski), mistaken for a millionaire Lebowski, seeks restitution for his ruined rug and enlists his bowling buddies to help get it.", DirectorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), Genre = "Comedy, Crime", ReleaseDate = new DateTimeOffset(new DateTime(1998, 3, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), Title = "The Big Lebowski" }, 78 | new { Id = new Guid("f9a16fee-4c49-41bb-87a1-bbaad0cd1174"), Description = "A tale of greed, deception, money, power, and murder occur between two best friends: a mafia enforcer and a casino executive, compete against each other over a gambling empire, and over a fast living and fast loving socialite.", DirectorId = new Guid("c19099ed-94db-44ba-885b-0ad7205d5e40"), Genre = "Crime, Drama", ReleaseDate = new DateTimeOffset(new DateTime(1995, 11, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), Title = "Casino" }, 79 | new { Id = new Guid("bb6a100a-053f-4bf8-b271-60ce3aae6eb5"), Description = "An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.", DirectorId = new Guid("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), Genre = "Drama", ReleaseDate = new DateTimeOffset(new DateTime(1999, 10, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), Title = "Fight Club" }, 80 | new { Id = new Guid("3d2880ae-5ba6-417c-845d-f4ebfd4bcac7"), Description = "A sole survivor tells of the twisty events leading up to a horrific gun battle on a boat, which began when five criminals met at a seemingly random police lineup.", DirectorId = new Guid("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), Genre = "Crime, Thriller", ReleaseDate = new DateTimeOffset(new DateTime(1995, 9, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), Title = "The Usual Suspects" }, 81 | new { Id = new Guid("26fcbcc4-b7f7-47fc-9382-740c12246b59"), Description = "A cyborg, identical to the one who failed to kill Sarah Connor, must now protect her teenage son, John Connor, from a more advanced and powerful cyborg.", DirectorId = new Guid("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), Genre = "Action, Sci-Fi", ReleaseDate = new DateTimeOffset(new DateTime(1991, 7, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), Title = "Terminator 2: Judgment Day" } 82 | ); 83 | }); 84 | 85 | modelBuilder.Entity("Movies.API.Entities.Movie", b => 86 | { 87 | b.HasOne("Movies.API.Entities.Director", "Director") 88 | .WithMany() 89 | .HasForeignKey("DirectorId") 90 | .OnDelete(DeleteBehavior.Cascade); 91 | }); 92 | #pragma warning restore 612, 618 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Movies.API/Models/Movie.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace Movies.API.Models 5 | { 6 | public class Movie 7 | { 8 | public Guid Id { get; set; } 9 | public string Title { get; set; } 10 | public string Description { get; set; } 11 | public string Genre { get; set; } 12 | public DateTimeOffset ReleaseDate { get; set; } 13 | public string Director { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Movies.API/Models/MovieForCreation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Text; 5 | 6 | namespace Movies.API.Models 7 | { 8 | public class MovieForCreation 9 | { 10 | [Required] 11 | [MaxLength(200)] 12 | public string Title { get; set; } 13 | 14 | [MaxLength(2000)] 15 | [MinLength(10)] 16 | public string Description { get; set; } 17 | 18 | [MaxLength(200)] 19 | public string Genre { get; set; } 20 | 21 | [Required] 22 | public DateTimeOffset? ReleaseDate { get; set; } 23 | 24 | [Required] 25 | public Guid? DirectorId { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Movies.API/Models/MovieForUpdate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Text; 5 | 6 | namespace Movies.API.Models 7 | { 8 | public class MovieForUpdate 9 | { 10 | [Required] 11 | [MaxLength(200)] 12 | public string Title { get; set; } 13 | 14 | [MaxLength(2000)] 15 | public string Description { get; set; } 16 | 17 | [MaxLength(200)] 18 | public string Genre { get; set; } 19 | 20 | [Required] 21 | public DateTimeOffset ReleaseDate { get; set; } 22 | 23 | [Required] 24 | public Guid DirectorId { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Movies.API/Models/Poster.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Movies.API.Models 6 | { 7 | public class Poster 8 | { 9 | public Guid Id { get; set; } 10 | public Guid MovieId { get; set; } 11 | public string Name { get; set; } 12 | public byte[] Bytes { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Movies.API/Models/PosterForCreation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Text; 5 | 6 | namespace Movies.API.Models 7 | { 8 | public class PosterForCreation 9 | { 10 | [Required] 11 | [MaxLength(200)] 12 | public string Name { get; set; } 13 | 14 | [Required] 15 | public byte[] Bytes { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Movies.API/Models/Trailer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Movies.API.Models 6 | { 7 | public class Trailer 8 | { 9 | public Guid Id { get; set; } 10 | public Guid MovieId { get; set; } 11 | public string Name { get; set; } 12 | public string Description { get; set; } 13 | public byte[] Bytes { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Movies.API/Models/TrailerForCreation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Text; 5 | 6 | namespace Movies.API.Models 7 | { 8 | public class TrailerForCreation 9 | { 10 | [Required] 11 | public Guid MovieId { get; set; } 12 | 13 | [Required] 14 | [MaxLength(200)] 15 | public string Name { get; set; } 16 | 17 | [MaxLength(1000)] 18 | public string Description { get; set; } 19 | 20 | [Required] 21 | public byte[] Bytes { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Movies.API/Movies.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Movies.API/Profiles/MoviesProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using System.Collections.Generic; 3 | 4 | namespace Movies.API 5 | { 6 | /// 7 | /// AutoMapper profile for working with Movie objects 8 | /// 9 | public class MoviesProfile : Profile 10 | { 11 | public MoviesProfile() 12 | { 13 | CreateMap() 14 | .ForMember(dest => dest.Director, opt => opt.MapFrom(src => 15 | $"{src.Director.FirstName} {src.Director.LastName}")); 16 | 17 | CreateMap(); 18 | 19 | CreateMap().ReverseMap(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Movies.API/Profiles/PostersProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace Movies.API.Profiles 4 | { 5 | /// 6 | /// AutoMapper profile for working with Poster objects 7 | /// 8 | public class PostersProfile : Profile 9 | { 10 | public PostersProfile() 11 | { 12 | CreateMap(); 13 | CreateMap(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Movies.API/Profiles/TrailersProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace Movies.API.Profiles 4 | { 5 | /// 6 | /// AutoMapper profile for working with Trailer objects 7 | /// 8 | public class TrailersProfile : Profile 9 | { 10 | public TrailersProfile() 11 | { 12 | CreateMap(); 13 | CreateMap(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Movies.API/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.EntityFrameworkCore; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Logging; 12 | using Movies.API.Contexts; 13 | 14 | namespace Movies.API 15 | { 16 | public class Program 17 | { 18 | public static void Main(string[] args) 19 | { 20 | var host = CreateWebHostBuilder(args).Build(); 21 | 22 | // For demo purposes: clear the database 23 | // and refill it with dummy data 24 | using (var scope = host.Services.CreateScope()) 25 | { 26 | try 27 | { 28 | var context = scope.ServiceProvider.GetService(); 29 | // delete the DB if it exists 30 | context.Database.EnsureDeleted(); 31 | // migrate the DB - this will also seed the DB with dummy data 32 | context.Database.Migrate(); 33 | } 34 | catch (Exception ex) 35 | { 36 | var logger = scope.ServiceProvider.GetRequiredService>(); 37 | logger.LogError(ex, "An error occurred while migrating the database."); 38 | } 39 | } 40 | 41 | // run the web app 42 | host.Run(); 43 | } 44 | 45 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 46 | WebHost.CreateDefaultBuilder(args) 47 | .UseStartup(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Movies.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:57863", 7 | "sslPort": 0 8 | } 9 | }, 10 | "$schema": "http://json.schemastore.org/launchsettings.json", 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchUrl": "http://localhost:57863", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "Movies.API": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "launchUrl": "api/values", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | }, 26 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Movies.API/Services/IMoviesRepository.cs: -------------------------------------------------------------------------------- 1 | using Movies.API.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Movies.API.Services 8 | { 9 | public interface IMoviesRepository 10 | { 11 | Task GetMovieAsync(Guid movieId); 12 | 13 | Task> GetMoviesAsync(); 14 | 15 | void UpdateMovie(Movie movieToUpdate); 16 | 17 | void AddMovie(Movie movieToAdd); 18 | 19 | void DeleteMovie(Movie movieToDelete); 20 | 21 | Task SaveChangesAsync(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Movies.API/Services/IPostersRepository.cs: -------------------------------------------------------------------------------- 1 | using Movies.API.InternalModels; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Movies.API.Services 8 | { 9 | public interface IPostersRepository 10 | { 11 | Task GetPosterAsync(Guid movieId, Guid posterId); 12 | 13 | Task AddPoster(Guid movieId, Poster posterToAdd); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Movies.API/Services/ITrailersRepository.cs: -------------------------------------------------------------------------------- 1 | using Movies.API.InternalModels; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Movies.API.Services 8 | { 9 | public interface ITrailersRepository 10 | { 11 | Task GetTrailerAsync(Guid movieId, Guid trailerId); 12 | 13 | Task AddTrailer(Guid movieId, Trailer trailerToAdd); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Movies.API/Services/MoviesRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Movies.API.Contexts; 3 | using Movies.API.Entities; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace Movies.API.Services 10 | { 11 | public class MoviesRepository : IMoviesRepository, IDisposable 12 | { 13 | private MoviesContext _context; 14 | 15 | public MoviesRepository(MoviesContext context) 16 | { 17 | _context = context ?? throw new ArgumentNullException(nameof(context)); 18 | } 19 | 20 | public async Task GetMovieAsync(Guid movieId) 21 | { 22 | return await _context.Movies.Include(m => m.Director) 23 | .FirstOrDefaultAsync(m => m.Id == movieId); 24 | } 25 | 26 | public async Task> GetMoviesAsync() 27 | { 28 | return await _context.Movies.Include(m => m.Director).ToListAsync(); 29 | } 30 | 31 | public void UpdateMovie(Movie movieToUpdate) 32 | { 33 | // no code required, entity tracked by context. Including 34 | // this is best practice to ensure other implementations of the 35 | // contract (eg a mock version) can execute code on update 36 | // when needed. 37 | } 38 | 39 | public void AddMovie(Movie movieToAdd) 40 | { 41 | if (movieToAdd == null) 42 | { 43 | throw new ArgumentNullException(nameof(movieToAdd)); 44 | } 45 | 46 | _context.Add(movieToAdd); 47 | } 48 | 49 | public void DeleteMovie(Movie movieToDelete) 50 | { 51 | if (movieToDelete == null) 52 | { 53 | throw new ArgumentNullException(nameof(movieToDelete)); 54 | } 55 | 56 | _context.Remove(movieToDelete); 57 | } 58 | 59 | public async Task SaveChangesAsync() 60 | { 61 | // return true if 1 or more entities were changed 62 | return (await _context.SaveChangesAsync() > 0); 63 | } 64 | 65 | public void Dispose() 66 | { 67 | Dispose(true); 68 | GC.SuppressFinalize(this); 69 | } 70 | 71 | protected virtual void Dispose(bool disposing) 72 | { 73 | if (disposing) 74 | { 75 | if (_context != null) 76 | { 77 | _context.Dispose(); 78 | _context = null; 79 | } 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Movies.API/Services/PostersRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore; 6 | using Movies.API.Contexts; 7 | using Movies.API.InternalModels; 8 | 9 | namespace Movies.API.Services 10 | { 11 | public class PostersRepository : IPostersRepository, IDisposable 12 | { 13 | private MoviesContext _context; 14 | 15 | public PostersRepository(MoviesContext context) 16 | { 17 | _context = context ?? throw new ArgumentNullException(nameof(context)); 18 | 19 | } 20 | 21 | public async Task GetPosterAsync(Guid movieId, Guid posterId) 22 | { 23 | // Generate the name from the movie title. 24 | var movie = await _context.Movies 25 | .FirstOrDefaultAsync(m => m.Id == movieId); 26 | 27 | if (movie == null) 28 | { 29 | throw new Exception($"Movie with id {movieId} not found."); 30 | } 31 | 32 | // generate a movie poster of 500KB 33 | var random = new Random(); 34 | var generatedBytes = new byte[524288]; 35 | random.NextBytes(generatedBytes); 36 | 37 | return new Poster() 38 | { 39 | Bytes = generatedBytes, 40 | Id = posterId, 41 | MovieId = movieId, 42 | Name = $"{movie.Title} poster number {DateTime.UtcNow.Ticks}" 43 | }; 44 | } 45 | 46 | public async Task AddPoster(Guid movieId, Poster posterToAdd) 47 | { 48 | // don't do anything: we're just faking this. Simply return the poster 49 | // after setting the ids 50 | posterToAdd.MovieId = movieId; 51 | posterToAdd.Id = Guid.NewGuid(); 52 | return await Task.FromResult(posterToAdd); 53 | } 54 | 55 | public void Dispose() 56 | { 57 | Dispose(true); 58 | GC.SuppressFinalize(this); 59 | } 60 | 61 | 62 | protected virtual void Dispose(bool disposing) 63 | { 64 | if (disposing) 65 | { 66 | if (_context != null) 67 | { 68 | _context.Dispose(); 69 | _context = null; 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Movies.API/Services/TrailersRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore; 6 | using Movies.API.Contexts; 7 | using Movies.API.InternalModels; 8 | 9 | namespace Movies.API.Services 10 | { 11 | public class TrailersRepository : ITrailersRepository, IDisposable 12 | { 13 | private MoviesContext _context; 14 | 15 | public TrailersRepository(MoviesContext context) 16 | { 17 | _context = context ?? throw new ArgumentNullException(nameof(context)); 18 | 19 | } 20 | 21 | public async Task GetTrailerAsync(Guid movieId, Guid trailerId) 22 | { 23 | // Generate the name from the movie title. 24 | var movie = await _context.Movies 25 | .FirstOrDefaultAsync(m => m.Id == movieId); 26 | 27 | if (movie == null) 28 | { 29 | throw new Exception($"Movie with id {movieId} not found."); 30 | } 31 | 32 | // generate a trailer (byte array) between 50 and 100MB 33 | var random = new Random(); 34 | var generatedByteLength = random.Next(52428800, 104857600); 35 | var generatedBytes = new byte[generatedByteLength]; 36 | random.NextBytes(generatedBytes); 37 | 38 | return new Trailer() 39 | { 40 | Bytes = generatedBytes, 41 | Id = trailerId, 42 | MovieId = movieId, 43 | Name = $"{movie.Title} trailer number {DateTime.UtcNow.Ticks}", 44 | Description = $"{movie.Title} trailer description {DateTime.UtcNow.Ticks}" 45 | }; 46 | } 47 | 48 | public async Task AddTrailer(Guid movieId, Trailer trailerToAdd) 49 | { 50 | // don't do anything: we're just faking this. Simply return the trailer 51 | // after setting the ids 52 | trailerToAdd.MovieId = movieId; 53 | trailerToAdd.Id = Guid.NewGuid(); 54 | return await Task.FromResult(trailerToAdd); 55 | } 56 | 57 | public void Dispose() 58 | { 59 | Dispose(true); 60 | GC.SuppressFinalize(this); 61 | } 62 | 63 | protected virtual void Dispose(bool disposing) 64 | { 65 | if (disposing) 66 | { 67 | if (_context != null) 68 | { 69 | _context.Dispose(); 70 | _context = null; 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Movies.API/Startup.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.Formatters; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Movies.API.Contexts; 10 | using Movies.API.Services; 11 | using Swashbuckle.AspNetCore.Swagger; 12 | 13 | namespace Movies.API 14 | { 15 | public class Startup 16 | { 17 | public Startup(IConfiguration configuration) 18 | { 19 | Configuration = configuration; 20 | } 21 | 22 | public IConfiguration Configuration { get; } 23 | 24 | // This method gets called by the runtime. Use this method to add services to the container. 25 | public void ConfigureServices(IServiceCollection services) 26 | { 27 | services.AddMvc(options => 28 | { 29 | // Return a 406 when an unsupported media type was requested 30 | options.ReturnHttpNotAcceptable = true; 31 | 32 | // Add XML formatters 33 | options.OutputFormatters.Add(new XmlSerializerOutputFormatter()); 34 | options.InputFormatters.Add(new XmlSerializerInputFormatter(options)); 35 | 36 | // Set XML as default format instead of JSON - the first formatter in the 37 | // list is the default, so we insert the input/output formatters at 38 | // position 0 39 | //options.OutputFormatters.Insert(0, new XmlSerializerOutputFormatter()); 40 | //options.InputFormatters.Insert(0, new XmlSerializerInputFormatter(options)); 41 | } 42 | ).SetCompatibilityVersion(CompatibilityVersion.Version_2_1); 43 | 44 | // add support for compressing responses (eg gzip) 45 | services.AddResponseCompression(); 46 | 47 | // suppress automatic model state validation when using the 48 | // ApiController attribute (as it will return a 400 Bad Request 49 | // instead of the more correct 422 Unprocessable Entity when 50 | // validation errors are encountered) 51 | services.Configure(options => 52 | { 53 | options.SuppressModelStateInvalidFilter = true; 54 | }); 55 | // register the DbContext on the container, getting the connection string from 56 | // appSettings (note: use this during development; in a production environment, 57 | // it's better to store the connection string in an environment variable) 58 | var connectionString = Configuration["ConnectionStrings:MoviesDBConnectionString"]; 59 | services.AddDbContext(o => o.UseSqlServer(connectionString)); 60 | 61 | services.AddScoped(); 62 | services.AddScoped(); 63 | services.AddScoped(); 64 | 65 | services.AddAutoMapper(); 66 | 67 | services.AddSwaggerGen(c => 68 | { 69 | c.SwaggerDoc("v1", new Info { Title = "Movies API", Version = "v1" }); 70 | }); 71 | } 72 | 73 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 74 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 75 | { 76 | // use response compression (client should pass through 77 | // Accept-Encoding) 78 | app.UseResponseCompression(); 79 | 80 | // Enable middleware to serve generated Swagger as a JSON endpoint. 81 | app.UseSwagger(); 82 | 83 | // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), 84 | // specifying the Swagger JSON endpoint. 85 | app.UseSwaggerUI(c => 86 | { 87 | c.SwaggerEndpoint("swagger/v1/swagger.json", "Movies API (v1)"); 88 | // serve UI at root 89 | c.RoutePrefix = string.Empty; 90 | }); 91 | 92 | if (env.IsDevelopment()) 93 | { 94 | app.UseDeveloperExceptionPage(); 95 | } 96 | 97 | app.UseMvc(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Movies.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Movies.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information" 5 | } 6 | }, 7 | "AllowedHosts": "*", 8 | "ConnectionStrings": { 9 | "MoviesDBConnectionString": "Server=(localdb)\\mssqllocaldb;Database=MoviesDB;Trusted_Connection=True;" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Movies.Client/Models/GeneratedDtoModels.cs: -------------------------------------------------------------------------------- 1 | //---------------------- 2 | // 3 | // Generated using the NSwag toolchain v11.20.1.0 (NJsonSchema v9.11.0.0 (Newtonsoft.Json v9.0.0.0)) (http://NSwag.org) 4 | // 5 | //---------------------- 6 | 7 | namespace Movies.Client.Models 8 | { 9 | #pragma warning disable // Disable all warnings 10 | 11 | 12 | 13 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "9.11.0.0 (Newtonsoft.Json v9.0.0.0)")] 14 | public partial class Movie 15 | { 16 | [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 17 | public System.Guid? Id { get; set; } 18 | 19 | [Newtonsoft.Json.JsonProperty("title", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 20 | public string Title { get; set; } 21 | 22 | [Newtonsoft.Json.JsonProperty("description", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 23 | public string Description { get; set; } 24 | 25 | [Newtonsoft.Json.JsonProperty("genre", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 26 | public string Genre { get; set; } 27 | 28 | [Newtonsoft.Json.JsonProperty("releaseDate", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 29 | public System.DateTimeOffset? ReleaseDate { get; set; } 30 | 31 | [Newtonsoft.Json.JsonProperty("director", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 32 | public string Director { get; set; } 33 | 34 | 35 | } 36 | 37 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "9.11.0.0 (Newtonsoft.Json v9.0.0.0)")] 38 | public partial class MovieForCreation 39 | { 40 | [Newtonsoft.Json.JsonProperty("title", Required = Newtonsoft.Json.Required.Always)] 41 | public string Title { get; set; } 42 | 43 | [Newtonsoft.Json.JsonProperty("description", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 44 | public string Description { get; set; } 45 | 46 | [Newtonsoft.Json.JsonProperty("genre", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 47 | public string Genre { get; set; } 48 | 49 | [Newtonsoft.Json.JsonProperty("releaseDate", Required = Newtonsoft.Json.Required.Always)] 50 | public System.DateTimeOffset ReleaseDate { get; set; } 51 | 52 | [Newtonsoft.Json.JsonProperty("directorId", Required = Newtonsoft.Json.Required.Always)] 53 | public System.Guid DirectorId { get; set; } 54 | 55 | 56 | } 57 | 58 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "9.11.0.0 (Newtonsoft.Json v9.0.0.0)")] 59 | public partial class MovieForUpdate 60 | { 61 | [Newtonsoft.Json.JsonProperty("title", Required = Newtonsoft.Json.Required.Always)] 62 | public string Title { get; set; } 63 | 64 | [Newtonsoft.Json.JsonProperty("description", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 65 | public string Description { get; set; } 66 | 67 | [Newtonsoft.Json.JsonProperty("genre", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 68 | public string Genre { get; set; } 69 | 70 | [Newtonsoft.Json.JsonProperty("releaseDate", Required = Newtonsoft.Json.Required.Always)] 71 | public System.DateTimeOffset ReleaseDate { get; set; } 72 | 73 | [Newtonsoft.Json.JsonProperty("directorId", Required = Newtonsoft.Json.Required.Always)] 74 | public System.Guid DirectorId { get; set; } 75 | 76 | 77 | } 78 | 79 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "9.11.0.0 (Newtonsoft.Json v9.0.0.0)")] 80 | public partial class Poster 81 | { 82 | [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 83 | public System.Guid? Id { get; set; } 84 | 85 | [Newtonsoft.Json.JsonProperty("movieId", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 86 | public System.Guid? MovieId { get; set; } 87 | 88 | [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 89 | public string Name { get; set; } 90 | 91 | [Newtonsoft.Json.JsonProperty("bytes", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 92 | public byte[] Bytes { get; set; } 93 | 94 | 95 | } 96 | 97 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "9.11.0.0 (Newtonsoft.Json v9.0.0.0)")] 98 | public partial class PosterForCreation 99 | { 100 | [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Always)] 101 | public string Name { get; set; } 102 | 103 | [Newtonsoft.Json.JsonProperty("bytes", Required = Newtonsoft.Json.Required.Always)] 104 | public byte[] Bytes { get; set; } 105 | 106 | 107 | } 108 | 109 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "9.11.0.0 (Newtonsoft.Json v9.0.0.0)")] 110 | public partial class Trailer 111 | { 112 | [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 113 | public System.Guid? Id { get; set; } 114 | 115 | [Newtonsoft.Json.JsonProperty("movieId", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 116 | public System.Guid? MovieId { get; set; } 117 | 118 | [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 119 | public string Name { get; set; } 120 | 121 | [Newtonsoft.Json.JsonProperty("description", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 122 | public string Description { get; set; } 123 | 124 | [Newtonsoft.Json.JsonProperty("bytes", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 125 | public byte[] Bytes { get; set; } 126 | 127 | 128 | } 129 | 130 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "9.11.0.0 (Newtonsoft.Json v9.0.0.0)")] 131 | public partial class TrailerForCreation 132 | { 133 | [Newtonsoft.Json.JsonProperty("movieId", Required = Newtonsoft.Json.Required.Always)] 134 | public System.Guid MovieId { get; set; } 135 | 136 | [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Always)] 137 | public string Name { get; set; } 138 | 139 | [Newtonsoft.Json.JsonProperty("description", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] 140 | public string Description { get; set; } 141 | 142 | [Newtonsoft.Json.JsonProperty("bytes", Required = Newtonsoft.Json.Required.Always)] 143 | public byte[] Bytes { get; set; } 144 | 145 | 146 | } 147 | 148 | [System.CodeDom.Compiler.GeneratedCode("NSwag", "11.20.1.0 (NJsonSchema v9.11.0.0 (Newtonsoft.Json v9.0.0.0))")] 149 | public partial class SwaggerException : System.Exception 150 | { 151 | public int StatusCode { get; private set; } 152 | 153 | public string Response { get; private set; } 154 | 155 | public System.Collections.Generic.Dictionary> Headers { get; private set; } 156 | 157 | public SwaggerException(string message, int statusCode, string response, System.Collections.Generic.Dictionary> headers, System.Exception innerException) 158 | : base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + response.Substring(0, response.Length >= 512 ? 512 : response.Length), innerException) 159 | { 160 | StatusCode = statusCode; 161 | Response = response; 162 | Headers = headers; 163 | } 164 | 165 | public override string ToString() 166 | { 167 | return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString()); 168 | } 169 | } 170 | 171 | [System.CodeDom.Compiler.GeneratedCode("NSwag", "11.20.1.0 (NJsonSchema v9.11.0.0 (Newtonsoft.Json v9.0.0.0))")] 172 | public partial class SwaggerException : SwaggerException 173 | { 174 | public TResult Result { get; private set; } 175 | 176 | public SwaggerException(string message, int statusCode, string response, System.Collections.Generic.Dictionary> headers, TResult result, System.Exception innerException) 177 | : base(message, statusCode, response, headers, innerException) 178 | { 179 | Result = result; 180 | } 181 | } 182 | 183 | } -------------------------------------------------------------------------------- /Movies.Client/Movies.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | 7 | 8 | 9 | latest 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Movies.Client/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Logging; 3 | using Movies.Client.Services; 4 | using System; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | 8 | namespace Movies.Client 9 | { 10 | class Program 11 | { 12 | 13 | static async Task Main(string[] args) 14 | { 15 | // create a new ServiceCollection 16 | var serviceCollection = new ServiceCollection(); 17 | 18 | ConfigureServices(serviceCollection); 19 | 20 | // create a new ServiceProvider 21 | var serviceProvider = serviceCollection.BuildServiceProvider(); 22 | 23 | // For demo purposes: overall catch-all to log any exception that might 24 | // happen to the console & wait for key input afterwards so we can easily 25 | // inspect the issue. 26 | try 27 | { 28 | // Run our IntegrationService containing all samples and 29 | // await this call to ensure the application doesn't 30 | // prematurely exit. 31 | await serviceProvider.GetService().Run(); 32 | } 33 | catch (Exception generalException) 34 | { 35 | // log the exception 36 | var logger = serviceProvider.GetService>(); 37 | logger.LogError(generalException, 38 | "An exception happened while running the integration service."); 39 | } 40 | 41 | Console.ReadKey(); 42 | } 43 | 44 | private static void ConfigureServices(IServiceCollection serviceCollection) 45 | { 46 | // add loggers 47 | serviceCollection.AddSingleton(new LoggerFactory() 48 | .AddConsole() 49 | .AddDebug()); 50 | 51 | serviceCollection.AddLogging(); 52 | 53 | // register the integration service on our container with a 54 | // scoped lifetime 55 | 56 | // For the stream demos 57 | // serviceCollection.AddScoped(); 58 | 59 | // For the compression demos 60 | // serviceCollection.AddScoped(); 61 | 62 | // For the cancellation demos 63 | // serviceCollection.AddScoped(); 64 | 65 | // For the custom http handlers demos 66 | // serviceCollection.AddScoped(); 67 | 68 | // For the HttpClientFactory demos 69 | serviceCollection.AddScoped(); 70 | 71 | 72 | #region HttpClientFactory 73 | serviceCollection.AddHttpClient(); 74 | #endregion 75 | 76 | #region HttpClientFactory named instance with defaults 77 | 78 | //serviceCollection.AddHttpClient("MoviesClient", client => 79 | //{ 80 | // client.BaseAddress = new Uri("http://localhost:57863"); 81 | // client.Timeout = new TimeSpan(0, 0, 30); 82 | // client.DefaultRequestHeaders.Clear(); 83 | //}); 84 | 85 | #endregion 86 | 87 | #region HttpClientFactory named instance with defaults, handlers & additional primary handler configuration 88 | 89 | serviceCollection.AddHttpClient("MoviesClient", client => 90 | { 91 | client.BaseAddress = new Uri("http://localhost:57863"); 92 | client.Timeout = new TimeSpan(0, 0, 30); 93 | client.DefaultRequestHeaders.Clear(); 94 | }) 95 | .AddHttpMessageHandler(handler => new TimeOutDelegatingHandler(TimeSpan.FromSeconds(20))) 96 | .ConfigurePrimaryHttpMessageHandler(handler => 97 | new HttpClientHandler() 98 | { 99 | AutomaticDecompression = System.Net.DecompressionMethods.GZip 100 | }); 101 | 102 | #endregion 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Movies.Client/Services/CancellationService.cs: -------------------------------------------------------------------------------- 1 | using Marvin.StreamExtensions; 2 | using Movies.Client.Models; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace Movies.Client.Services 12 | { 13 | public class CancellationService : IIntegrationService 14 | { 15 | private HttpClient _httpClient = new HttpClient( 16 | new HttpClientHandler() 17 | { 18 | AutomaticDecompression = System.Net.DecompressionMethods.GZip 19 | }); 20 | 21 | private CancellationTokenSource _cancellationTokenSource = 22 | new CancellationTokenSource(); 23 | 24 | public CancellationService() 25 | { 26 | // set up HttpClient instance 27 | _httpClient.BaseAddress = new Uri("http://localhost:57863"); 28 | //_httpClient.Timeout = new TimeSpan(0, 0, 30); 29 | // for timeout demo 30 | _httpClient.Timeout = new TimeSpan(0, 0, 2); 31 | _httpClient.DefaultRequestHeaders.Clear(); 32 | } 33 | 34 | public async Task Run() 35 | { 36 | _cancellationTokenSource.CancelAfter(1000); 37 | // await GetTrailerAndCancel(_cancellationTokenSource.Token); 38 | await GetTrailerAndHandleTimeout(); 39 | } 40 | 41 | private async Task GetTrailerAndCancel(CancellationToken cancellationToken) 42 | { 43 | var request = new HttpRequestMessage( 44 | HttpMethod.Get, 45 | $"api/movies/d8663e5e-7494-4f81-8739-6e0de1bea7ee/trailers/{Guid.NewGuid()}"); 46 | 47 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 48 | request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); 49 | 50 | try 51 | { 52 | using (var response = await _httpClient.SendAsync(request, 53 | HttpCompletionOption.ResponseHeadersRead, 54 | cancellationToken)) 55 | { 56 | var stream = await response.Content.ReadAsStreamAsync(); 57 | 58 | response.EnsureSuccessStatusCode(); 59 | var trailer = stream.ReadAndDeserializeFromJson(); 60 | } 61 | } 62 | catch (OperationCanceledException ocException) 63 | { 64 | Console.WriteLine($"An operation was cancelled with message {ocException.Message}."); 65 | // additional cleanup, ... 66 | } 67 | } 68 | 69 | private async Task GetTrailerAndHandleTimeout() 70 | { 71 | var request = new HttpRequestMessage( 72 | HttpMethod.Get, 73 | $"api/movies/d8663e5e-7494-4f81-8739-6e0de1bea7ee/trailers/{Guid.NewGuid()}"); 74 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 75 | request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); 76 | 77 | try 78 | { 79 | using (var response = await _httpClient.SendAsync(request, 80 | HttpCompletionOption.ResponseHeadersRead)) 81 | { 82 | var stream = await response.Content.ReadAsStreamAsync(); 83 | 84 | response.EnsureSuccessStatusCode(); 85 | var trailer = stream.ReadAndDeserializeFromJson(); 86 | } 87 | } 88 | catch (OperationCanceledException ocException) 89 | { 90 | Console.WriteLine($"An operation was cancelled with message {ocException.Message}."); 91 | // additional cleanup, ... 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Movies.Client/Services/CompressionService.cs: -------------------------------------------------------------------------------- 1 | using Marvin.StreamExtensions; 2 | using Movies.Client.Models; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace Movies.Client.Services 11 | { 12 | public class CompressionService : IIntegrationService 13 | { 14 | private HttpClient _httpClient = new HttpClient(); 15 | 16 | // enable automatic decompression 17 | //private HttpClient _httpClient = new HttpClient( 18 | // new HttpClientHandler() 19 | // { 20 | // AutomaticDecompression = System.Net.DecompressionMethods.GZip 21 | // }); 22 | 23 | public CompressionService() 24 | { 25 | // set up HttpClient instance 26 | _httpClient.BaseAddress = new Uri("http://localhost:57863"); 27 | _httpClient.Timeout = new TimeSpan(0, 0, 30); 28 | _httpClient.DefaultRequestHeaders.Clear(); 29 | } 30 | 31 | public async Task Run() 32 | { 33 | await GetPosterWithGZipCompression(); 34 | } 35 | 36 | 37 | private async Task GetPosterWithGZipCompression() 38 | { 39 | var request = new HttpRequestMessage( 40 | HttpMethod.Get, 41 | $"api/movies/d8663e5e-7494-4f81-8739-6e0de1bea7ee/posters/{Guid.NewGuid()}"); 42 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 43 | request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); 44 | 45 | using (var response = await _httpClient.SendAsync(request)) 46 | { 47 | response.EnsureSuccessStatusCode(); 48 | 49 | var stream = await response.Content.ReadAsStreamAsync(); 50 | var poster = stream.ReadAndDeserializeFromJson(); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Movies.Client/Services/HttpClientFactoryInstanceManagementService.cs: -------------------------------------------------------------------------------- 1 | using Marvin.StreamExtensions; 2 | using Movies.Client.Models; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace Movies.Client.Services 12 | { 13 | public class HttpClientFactoryInstanceManagementService : IIntegrationService 14 | { 15 | private readonly CancellationTokenSource _cancellationTokenSource = 16 | new CancellationTokenSource(); 17 | 18 | private readonly IHttpClientFactory _httpClientFactory; 19 | 20 | public HttpClientFactoryInstanceManagementService(IHttpClientFactory httpClientFactory) 21 | { 22 | _httpClientFactory = httpClientFactory; 23 | } 24 | 25 | public async Task Run() 26 | { 27 | //await GetMoviesWithHttpClientFromFactory(_cancellationTokenSource.Token); 28 | await GetMoviesWithNamedHttpClientFromFactory(_cancellationTokenSource.Token); 29 | } 30 | 31 | private async Task GetMoviesWithHttpClientFromFactory( 32 | CancellationToken cancellationToken) 33 | { 34 | var httpClient = _httpClientFactory.CreateClient(); 35 | 36 | var request = new HttpRequestMessage( 37 | HttpMethod.Get, 38 | "http://localhost:57863/api/movies"); 39 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 40 | 41 | using (var response = await httpClient.SendAsync(request, 42 | HttpCompletionOption.ResponseHeadersRead, 43 | cancellationToken)) 44 | { 45 | var stream = await response.Content.ReadAsStreamAsync(); 46 | response.EnsureSuccessStatusCode(); 47 | var movies = stream.ReadAndDeserializeFromJson>(); 48 | } 49 | 50 | } 51 | 52 | private async Task GetMoviesWithNamedHttpClientFromFactory( 53 | CancellationToken cancellationToken) 54 | { 55 | var httpClient = _httpClientFactory.CreateClient("MoviesClient"); 56 | 57 | var request = new HttpRequestMessage( 58 | HttpMethod.Get, 59 | "api/movies"); 60 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 61 | request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); 62 | 63 | using (var response = await httpClient.SendAsync(request, 64 | HttpCompletionOption.ResponseHeadersRead, 65 | cancellationToken)) 66 | { 67 | var stream = await response.Content.ReadAsStreamAsync(); 68 | response.EnsureSuccessStatusCode(); 69 | var movies = stream.ReadAndDeserializeFromJson>(); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Movies.Client/Services/HttpHandlersService.cs: -------------------------------------------------------------------------------- 1 | using Marvin.StreamExtensions; 2 | using Movies.Client.Models; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace Movies.Client.Services 12 | { 13 | public class HttpHandlersService : IIntegrationService 14 | { 15 | private CancellationTokenSource _cancellationTokenSource = 16 | new CancellationTokenSource(); 17 | 18 | private HttpClient _httpClient = 19 | new HttpClient( 20 | new TimeOutDelegatingHandler( 21 | new HttpClientHandler() 22 | { 23 | AutomaticDecompression = System.Net.DecompressionMethods.GZip 24 | }, 25 | new TimeSpan(0,0,2) 26 | )); 27 | 28 | 29 | public HttpHandlersService() 30 | { 31 | // set up HttpClient instance 32 | _httpClient.BaseAddress = new Uri("http://localhost:57863"); 33 | _httpClient.Timeout = new TimeSpan(0, 0, 30); 34 | _httpClient.DefaultRequestHeaders.Clear(); 35 | } 36 | 37 | public async Task Run() 38 | { 39 | await GetMovies(_cancellationTokenSource.Token); 40 | } 41 | 42 | public async Task GetMovies(CancellationToken cancellationToken) 43 | { 44 | var request = new HttpRequestMessage( 45 | HttpMethod.Get, 46 | "api/movies/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"); 47 | 48 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 49 | request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); 50 | 51 | using (var response = await _httpClient.SendAsync(request, 52 | HttpCompletionOption.ResponseHeadersRead, 53 | cancellationToken)) 54 | { 55 | response.EnsureSuccessStatusCode(); 56 | 57 | var stream = await response.Content.ReadAsStreamAsync(); 58 | var movie = stream.ReadAndDeserializeFromJson(); 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Movies.Client/Services/IIntegrationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace Movies.Client.Services 7 | { 8 | public interface IIntegrationService 9 | { 10 | Task Run(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Movies.Client/Services/StreamService.cs: -------------------------------------------------------------------------------- 1 | using Movies.Client.Models; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Net.Http; 8 | using System.Net.Http.Headers; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using Marvin.StreamExtensions; 12 | 13 | namespace Movies.Client.Services 14 | { 15 | public class StreamService : IIntegrationService 16 | { 17 | private HttpClient _httpClient = new HttpClient(); 18 | 19 | public StreamService() 20 | { 21 | // set up HttpClient instance 22 | _httpClient.BaseAddress = new Uri("http://localhost:57863"); 23 | _httpClient.Timeout = new TimeSpan(0, 0, 30); 24 | _httpClient.DefaultRequestHeaders.Clear(); 25 | } 26 | 27 | public async Task Run() 28 | { 29 | //await GetPosterWithoutStream(); 30 | 31 | // await GetPosterWithStream(); 32 | 33 | // await GetPosterWithStreamAndCompletionMode(); 34 | 35 | await PostPosterWithStream(); 36 | 37 | //await PostAndReadPosterWithStreams(); 38 | 39 | //await PostAndReadPosterWithStreamsUsingExtensionMethods(); 40 | 41 | } 42 | 43 | private async Task GetPosterWithoutStream() 44 | { 45 | var request = new HttpRequestMessage( 46 | HttpMethod.Get, 47 | $"api/movies/d8663e5e-7494-4f81-8739-6e0de1bea7ee/posters/{Guid.NewGuid()}"); 48 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 49 | 50 | var response = await _httpClient.SendAsync(request); 51 | response.EnsureSuccessStatusCode(); 52 | 53 | var content = await response.Content.ReadAsStringAsync(); 54 | var poster = JsonConvert.DeserializeObject(content); 55 | } 56 | 57 | 58 | private async Task GetPosterWithStream() 59 | { 60 | var request = new HttpRequestMessage( 61 | HttpMethod.Get, 62 | $"api/movies/d8663e5e-7494-4f81-8739-6e0de1bea7ee/posters/{Guid.NewGuid()}"); 63 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 64 | 65 | using (var response = await _httpClient.SendAsync(request)) 66 | { 67 | response.EnsureSuccessStatusCode(); 68 | 69 | var stream = await response.Content.ReadAsStreamAsync(); 70 | 71 | using (var streamReader = new StreamReader(stream)) 72 | { 73 | using (var jsonTextReader = new JsonTextReader(streamReader)) 74 | { 75 | var jsonSerializer = new JsonSerializer(); 76 | var poster = jsonSerializer.Deserialize(jsonTextReader); 77 | 78 | // do something with the poster 79 | } 80 | } 81 | } 82 | } 83 | 84 | private async Task GetPosterWithStreamAndCompletionMode() 85 | { 86 | var request = new HttpRequestMessage( 87 | HttpMethod.Get, 88 | $"api/movies/d8663e5e-7494-4f81-8739-6e0de1bea7ee/posters/{Guid.NewGuid()}"); 89 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 90 | 91 | using (var response = await _httpClient.SendAsync(request, 92 | HttpCompletionOption.ResponseHeadersRead)) 93 | { 94 | response.EnsureSuccessStatusCode(); 95 | 96 | var stream = await response.Content.ReadAsStreamAsync(); 97 | 98 | using (var streamReader = new StreamReader(stream)) 99 | { 100 | using (var jsonTextReader = new JsonTextReader(streamReader)) 101 | { 102 | var jsonSerializer = new JsonSerializer(); 103 | var poster = jsonSerializer.Deserialize(jsonTextReader); 104 | 105 | // do something with the poster 106 | } 107 | } 108 | } 109 | } 110 | 111 | 112 | private async Task PostPosterWithStream() 113 | { 114 | // generate a movie poster of 500KB 115 | var random = new Random(); 116 | var generatedBytes = new byte[524288]; 117 | random.NextBytes(generatedBytes); 118 | 119 | var posterForCreation = new PosterForCreation() 120 | { 121 | Name = "A new poster for The Big Lebowski", 122 | Bytes = generatedBytes 123 | }; 124 | 125 | var memoryContentStream = new MemoryStream(); 126 | 127 | using (var streamWriter = new StreamWriter(memoryContentStream, 128 | new UTF8Encoding(), 1024, true)) 129 | { 130 | using (var jsonTextWriter = new JsonTextWriter(streamWriter)) 131 | { 132 | var jsonSerializer = new JsonSerializer(); 133 | jsonSerializer.Serialize(jsonTextWriter, posterForCreation); 134 | jsonTextWriter.Flush(); 135 | } 136 | } 137 | 138 | memoryContentStream.Seek(0, SeekOrigin.Begin); 139 | 140 | using (var request = new HttpRequestMessage( 141 | HttpMethod.Post, 142 | $"api/movies/d8663e5e-7494-4f81-8739-6e0de1bea7ee/posters")) 143 | { 144 | request.Headers.Accept.Add( 145 | new MediaTypeWithQualityHeaderValue("application/json")); 146 | 147 | using (var streamContent = new StreamContent(memoryContentStream)) 148 | { 149 | request.Content = streamContent; 150 | request.Content.Headers.ContentType = 151 | new MediaTypeHeaderValue("application/json"); 152 | 153 | var response = await _httpClient.SendAsync(request); 154 | response.EnsureSuccessStatusCode(); 155 | 156 | var createdContent = await response.Content.ReadAsStringAsync(); 157 | var createdPoster = JsonConvert.DeserializeObject(createdContent); 158 | } 159 | } 160 | } 161 | 162 | private async Task PostAndReadPosterWithStreams() 163 | { 164 | // generate a movie poster of 500KB 165 | var random = new Random(); 166 | var generatedBytes = new byte[524288]; 167 | random.NextBytes(generatedBytes); 168 | 169 | var posterForCreation = new PosterForCreation() 170 | { 171 | Name = "A new poster for The Big Lebowski", 172 | Bytes = generatedBytes 173 | }; 174 | 175 | var memoryContentStream = new MemoryStream(); 176 | 177 | using (var streamWriter = new StreamWriter(memoryContentStream, 178 | new UTF8Encoding(), 1024, true)) 179 | { 180 | using (var jsonTextWriter = new JsonTextWriter(streamWriter)) 181 | { 182 | var jsonSerializer = new JsonSerializer(); 183 | jsonSerializer.Serialize(jsonTextWriter, posterForCreation); 184 | jsonTextWriter.Flush(); 185 | } 186 | } 187 | 188 | memoryContentStream.Seek(0, SeekOrigin.Begin); 189 | 190 | using (var request = new HttpRequestMessage( 191 | HttpMethod.Post, 192 | $"api/movies/d8663e5e-7494-4f81-8739-6e0de1bea7ee/posters")) 193 | { 194 | request.Headers.Accept.Add( 195 | new MediaTypeWithQualityHeaderValue("application/json")); 196 | 197 | using (var streamContent = new StreamContent(memoryContentStream)) 198 | { 199 | request.Content = streamContent; 200 | request.Content.Headers.ContentType = 201 | new MediaTypeHeaderValue("application/json"); 202 | 203 | using (var response = await _httpClient 204 | .SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) 205 | { 206 | response.EnsureSuccessStatusCode(); 207 | var stream = await response.Content.ReadAsStreamAsync(); 208 | 209 | using (var streamReader = new StreamReader(stream)) 210 | { 211 | using (var jsonTextReader = new JsonTextReader(streamReader)) 212 | { 213 | var jsonSerializer = new JsonSerializer(); 214 | var poster = jsonSerializer.Deserialize(jsonTextReader); 215 | 216 | // do something with the poster 217 | } 218 | } 219 | } 220 | } 221 | } 222 | } 223 | 224 | private async Task PostAndReadPosterWithStreamsUsingExtensionMethods() 225 | { 226 | // generate a movie poster of 500KB 227 | var random = new Random(); 228 | var generatedBytes = new byte[524288]; 229 | random.NextBytes(generatedBytes); 230 | 231 | var posterForCreation = new PosterForCreation() 232 | { 233 | Name = "A new poster for The Big Lebowski", 234 | Bytes = generatedBytes 235 | }; 236 | 237 | var memoryContentStream = new MemoryStream(); 238 | memoryContentStream.SerializeToJsonAndWrite(posterForCreation, 239 | new UTF8Encoding(), 1024, true); 240 | 241 | memoryContentStream.Seek(0, SeekOrigin.Begin); 242 | 243 | using (var request = new HttpRequestMessage( 244 | HttpMethod.Post, 245 | $"api/movies/d8663e5e-7494-4f81-8739-6e0de1bea7ee/posters")) 246 | { 247 | request.Headers.Accept.Add( 248 | new MediaTypeWithQualityHeaderValue("application/json")); 249 | 250 | using (var streamContent = new StreamContent(memoryContentStream)) 251 | { 252 | request.Content = streamContent; 253 | request.Content.Headers.ContentType = 254 | new MediaTypeHeaderValue("application/json"); 255 | 256 | using (var response = await _httpClient 257 | .SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) 258 | { 259 | response.EnsureSuccessStatusCode(); 260 | 261 | var stream = await response.Content.ReadAsStreamAsync(); 262 | var poster = stream.ReadAndDeserializeFromJson(); 263 | } 264 | } 265 | } 266 | } 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /Movies.Client/TimeOutDelegatingHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Movies.Client 9 | { 10 | public class TimeOutDelegatingHandler : DelegatingHandler 11 | { 12 | private readonly TimeSpan _timeOut = TimeSpan.FromSeconds(100); 13 | 14 | public TimeOutDelegatingHandler(TimeSpan timeOut) 15 | : base() 16 | { 17 | _timeOut = timeOut; 18 | } 19 | 20 | public TimeOutDelegatingHandler(HttpMessageHandler innerHandler, 21 | TimeSpan timeOut) 22 | : base(innerHandler) 23 | { 24 | _timeOut = timeOut; 25 | } 26 | 27 | protected async override Task SendAsync( 28 | HttpRequestMessage request, CancellationToken cancellationToken) 29 | { 30 | using (var linkedCancellationTokenSource = 31 | CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) 32 | { 33 | linkedCancellationTokenSource.CancelAfter(_timeOut); 34 | try 35 | { 36 | return await base.SendAsync(request, linkedCancellationTokenSource.Token); 37 | } 38 | catch (OperationCanceledException ex) 39 | { 40 | if (!cancellationToken.IsCancellationRequested) 41 | { 42 | throw new TimeoutException("The request timed out.", ex); 43 | } 44 | throw; 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Movies.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28803.352 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Movies.Client", "Movies.Client\Movies.Client.csproj", "{C0FA8F85-2A8D-45A4-905C-6665B21E4E91}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Movies.API", "Movies.API\Movies.API.csproj", "{8614CB18-9723-4EC7-8D41-D34BDECAD0ED}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "01 Client", "01 Client", "{C2BDCED0-499B-4001-8D36-DFB0DBE624B4}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "02 Server", "02 Server", "{CC16F37C-017E-4782-B335-12A2125D09E2}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {C0FA8F85-2A8D-45A4-905C-6665B21E4E91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {C0FA8F85-2A8D-45A4-905C-6665B21E4E91}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {C0FA8F85-2A8D-45A4-905C-6665B21E4E91}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {C0FA8F85-2A8D-45A4-905C-6665B21E4E91}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {8614CB18-9723-4EC7-8D41-D34BDECAD0ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {8614CB18-9723-4EC7-8D41-D34BDECAD0ED}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {8614CB18-9723-4EC7-8D41-D34BDECAD0ED}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {8614CB18-9723-4EC7-8D41-D34BDECAD0ED}.Release|Any CPU.Build.0 = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(SolutionProperties) = preSolution 30 | HideSolutionNode = FALSE 31 | EndGlobalSection 32 | GlobalSection(NestedProjects) = preSolution 33 | {C0FA8F85-2A8D-45A4-905C-6665B21E4E91} = {C2BDCED0-499B-4001-8D36-DFB0DBE624B4} 34 | {8614CB18-9723-4EC7-8D41-D34BDECAD0ED} = {CC16F37C-017E-4782-B335-12A2125D09E2} 35 | EndGlobalSection 36 | GlobalSection(ExtensibilityGlobals) = postSolution 37 | SolutionGuid = {CF9D6D81-D749-4D51-AB81-EED468744611} 38 | EndGlobalSection 39 | EndGlobal 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # An In-depth Look at HttpClient 2 | Code for my "An in-depth look at HttpClient" session at Techorama 2019 3 | 4 | Description of the session: 5 | 6 | "Applications, ranging from ASP.NET Core web apps over mobile Xamarin apps and Windows apps to Console apps often integrate with an API. For that, HttpClient is the default and best option. But there’s a lot more to using it than just sending a request and reading out the response. In this session you’ll get an in depth look at using HttpClient, from the internals over improving basic CRUD interaction with streams to working with compression. You'll also learn how and why to cancel requests that are no longer needed. 7 | 8 | Additionally, you’ll learn how to improve the reliability of your application with custom HttpMessageHandlers, and you'll learn how (and why) to use the new HttpClientFactory for HttpClient instance management." 9 | --------------------------------------------------------------------------------