├── Finished sample ├── Movies.Tests │ ├── Usings.cs │ ├── TestableClassWithApiAccessUnitTests.cs │ └── Movies.Tests.csproj ├── Movies.API │ ├── Movies.db │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── Controllers │ │ ├── FilmsController.cs │ │ ├── MoviesStreamController.cs │ │ ├── PostersController.cs │ │ ├── TrailersController.cs │ │ └── MoviesController.cs │ ├── Services │ │ ├── IPostersRepository.cs │ │ ├── ITrailersRepository.cs │ │ ├── IMoviesRepository.cs │ │ ├── PostersRepository.cs │ │ ├── TrailersRepository.cs │ │ └── MoviesRepository.cs │ ├── Profiles │ │ ├── PostersProfile.cs │ │ ├── TrailersProfile.cs │ │ └── MoviesProfile.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Models │ │ ├── Poster.cs │ │ ├── PosterForCreation.cs │ │ ├── Trailer.cs │ │ ├── TrailerForCreation.cs │ │ ├── Movie.cs │ │ ├── MovieForCreation.cs │ │ └── MovieForUpdate.cs │ ├── InternalModels │ │ ├── Poster.cs │ │ └── Trailer.cs │ ├── Movies.API.csproj │ ├── Entities │ │ ├── Director.cs │ │ └── Movie.cs │ ├── Program.cs │ ├── DbContexts │ │ └── MoviesDbContext.cs │ └── Migrations │ │ ├── InitialMigration.cs │ │ ├── MoviesDbContextModelSnapshot.cs │ │ └── InitialMigration.Designer.cs ├── Movies.Client │ ├── Services │ │ ├── IIntegrationService.cs │ │ ├── RemoteStreamingSamples.cs │ │ ├── CustomMessageHandlersSamples.cs │ │ ├── PartialUpdateSamples.cs │ │ ├── CancellationSamples.cs │ │ ├── HttpClientFactorySamples.cs │ │ ├── CompressionSamples.cs │ │ ├── FaultsAndErrorsSamples.cs │ │ └── CRUDSamples.cs │ ├── Models │ │ └── ExtendedProblemDetailsWithErrors.cs │ ├── Helpers │ │ └── JsonSerializerOptionsWrapper.cs │ ├── Handlers │ │ ├── Return401UnauthorizedResponseHandler.cs │ │ └── RetryPolicyDelegatingHandler.cs │ ├── UnauthorizedApiAccessException.cs │ ├── Movies.Client.csproj │ ├── MoviesAPIClient.cs │ ├── TestableClassWithApiAccess.cs │ └── Program.cs └── Movies.sln ├── Starter files ├── Movies.API │ ├── Movies.db │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── Controllers │ │ ├── FilmsController.cs │ │ ├── MoviesStreamController.cs │ │ ├── PostersController.cs │ │ ├── TrailersController.cs │ │ └── MoviesController.cs │ ├── Services │ │ ├── IPostersRepository.cs │ │ ├── ITrailersRepository.cs │ │ ├── IMoviesRepository.cs │ │ ├── PostersRepository.cs │ │ ├── TrailersRepository.cs │ │ └── MoviesRepository.cs │ ├── Profiles │ │ ├── PostersProfile.cs │ │ ├── TrailersProfile.cs │ │ └── MoviesProfile.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Models │ │ ├── Poster.cs │ │ ├── PosterForCreation.cs │ │ ├── Trailer.cs │ │ ├── TrailerForCreation.cs │ │ ├── Movie.cs │ │ ├── MovieForCreation.cs │ │ └── MovieForUpdate.cs │ ├── InternalModels │ │ ├── Poster.cs │ │ └── Trailer.cs │ ├── Movies.API.csproj │ ├── Entities │ │ ├── Director.cs │ │ └── Movie.cs │ ├── Program.cs │ ├── DbContexts │ │ └── MoviesDbContext.cs │ └── Migrations │ │ ├── InitialMigration.cs │ │ ├── MoviesDbContextModelSnapshot.cs │ │ └── InitialMigration.Designer.cs ├── Movies.Client │ ├── Services │ │ ├── IIntegrationService.cs │ │ ├── CRUDSamples.cs │ │ ├── FaultsAndErrorsSamples.cs │ │ ├── CancellationSamples.cs │ │ ├── LocalStreamsSamples.cs │ │ ├── CompressionSamples.cs │ │ ├── PartialUpdateSamples.cs │ │ ├── HttpClientFactorySamples.cs │ │ ├── RemoteStreamingSamples.cs │ │ └── CustomMessageHandlersSamples.cs │ ├── Movies.Client.csproj │ └── Program.cs └── Movies.sln ├── README.md ├── LICENSE └── .gitignore /Finished sample/Movies.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /Starter files/Movies.API/Movies.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinDockx/AccessingApisWithHttpClient/HEAD/Starter files/Movies.API/Movies.db -------------------------------------------------------------------------------- /Finished sample/Movies.API/Movies.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinDockx/AccessingApisWithHttpClient/HEAD/Finished sample/Movies.API/Movies.db -------------------------------------------------------------------------------- /Finished sample/Movies.Client/Services/IIntegrationService.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.Client.Services; 2 | public interface IIntegrationService 3 | { 4 | Task RunAsync(); 5 | } -------------------------------------------------------------------------------- /Starter files/Movies.Client/Services/IIntegrationService.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.Client.Services; 2 | public interface IIntegrationService 3 | { 4 | Task RunAsync(); 5 | } -------------------------------------------------------------------------------- /Finished sample/Movies.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Starter files/Movies.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Starter files/Movies.Client/Services/CRUDSamples.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.Client.Services; 2 | 3 | public class CRUDSamples : IIntegrationService 4 | { 5 | public Task RunAsync() 6 | { 7 | throw new NotImplementedException(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Starter files/Movies.Client/Services/FaultsAndErrorsSamples.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.Client.Services; 2 | 3 | public class FaultsAndErrorsSamples : IIntegrationService 4 | { 5 | public Task RunAsync() 6 | { 7 | throw new NotImplementedException(); 8 | } 9 | } -------------------------------------------------------------------------------- /Starter files/Movies.Client/Services/CancellationSamples.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.Client.Services; 2 | 3 | public class CancellationSamples : IIntegrationService 4 | { 5 | public Task RunAsync() 6 | { 7 | throw new NotImplementedException(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Starter files/Movies.Client/Services/LocalStreamsSamples.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.Client.Services; 2 | 3 | public class LocalStreamsSamples : IIntegrationService 4 | { 5 | public Task RunAsync() 6 | { 7 | throw new NotImplementedException(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Starter files/Movies.Client/Services/CompressionSamples.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.Client.Services; 2 | 3 | public class CompressionSamples : IIntegrationService 4 | { 5 | public Task RunAsync() 6 | { 7 | throw new NotImplementedException(); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /Starter files/Movies.Client/Services/PartialUpdateSamples.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.Client.Services; 2 | 3 | public class PartialUpdateSamples : IIntegrationService 4 | { 5 | public Task RunAsync() 6 | { 7 | throw new NotImplementedException(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Starter files/Movies.Client/Services/HttpClientFactorySamples.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.Client.Services; 2 | 3 | public class HttpClientFactorySamples : IIntegrationService 4 | { 5 | public Task RunAsync() 6 | { 7 | throw new NotImplementedException(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Starter files/Movies.Client/Services/RemoteStreamingSamples.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.Client.Services; 2 | 3 | public class RemoteStreamingSamples : IIntegrationService 4 | { 5 | public Task RunAsync() 6 | { 7 | throw new NotImplementedException(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Finished sample/Movies.Client/Models/ExtendedProblemDetailsWithErrors.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Movies.Client.Models; 4 | 5 | public class ExtendedProblemDetailsWithErrors : ProblemDetails 6 | { 7 | public Dictionary Errors { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /Starter files/Movies.Client/Services/CustomMessageHandlersSamples.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.Client.Services; 2 | 3 | public class CustomMessageHandlersSamples : IIntegrationService 4 | { 5 | public Task RunAsync() 6 | { 7 | throw new NotImplementedException(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "ConnectionStrings": { 10 | "MoviesDBConnectionString": "Data Source=Movies.db" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Starter files/Movies.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "ConnectionStrings": { 10 | "MoviesDBConnectionString": "Data Source=Movies.db" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Controllers/FilmsController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Movies.API.Controllers 4 | { 5 | [Route("api/films")] 6 | [ApiController] 7 | public class FilmsController : ControllerBase 8 | { 9 | [HttpGet] 10 | public IActionResult GetFilms() 11 | { 12 | return RedirectToAction("GetMovies", "Movies"); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Controllers/FilmsController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Movies.API.Controllers 4 | { 5 | [Route("api/films")] 6 | [ApiController] 7 | public class FilmsController : ControllerBase 8 | { 9 | [HttpGet] 10 | public IActionResult GetFilms() 11 | { 12 | return RedirectToAction("GetMovies", "Movies"); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Finished sample/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 | -------------------------------------------------------------------------------- /Finished sample/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 | -------------------------------------------------------------------------------- /Starter files/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 | -------------------------------------------------------------------------------- /Starter files/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 | -------------------------------------------------------------------------------- /Starter files/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 | -------------------------------------------------------------------------------- /Starter files/Movies.Client/Movies.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Finished sample/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 | -------------------------------------------------------------------------------- /Finished sample/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 | -------------------------------------------------------------------------------- /Finished sample/Movies.Client/Helpers/JsonSerializerOptionsWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace Movies.Client.Helpers; 4 | 5 | public class JsonSerializerOptionsWrapper 6 | { 7 | public JsonSerializerOptions Options { get; } 8 | 9 | public JsonSerializerOptionsWrapper() 10 | { 11 | Options = new JsonSerializerOptions( 12 | JsonSerializerDefaults.Web); 13 | Options.DefaultBufferSize = 10; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Starter files/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 | -------------------------------------------------------------------------------- /Finished sample/Movies.Client/Handlers/Return401UnauthorizedResponseHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.Client.Handlers; 2 | 3 | public class Return401UnauthorizedResponseHandler : HttpMessageHandler 4 | { 5 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 6 | { 7 | var response = new HttpResponseMessage( 8 | System.Net.HttpStatusCode.Unauthorized); 9 | return Task.FromResult(response); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Services/IMoviesRepository.cs: -------------------------------------------------------------------------------- 1 | using Movies.API.Entities; 2 | 3 | namespace Movies.API.Services; 4 | 5 | public interface IMoviesRepository 6 | { 7 | Task GetMovieAsync(Guid movieId); 8 | 9 | Task> GetMoviesAsync(); 10 | 11 | IAsyncEnumerable GetMoviesAsAsyncEnumerable(); 12 | 13 | void UpdateMovie(Movie movieToUpdate); 14 | 15 | void AddMovie(Movie movieToAdd); 16 | 17 | void DeleteMovie(Movie movieToDelete); 18 | 19 | Task SaveChangesAsync(); 20 | } 21 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Services/IMoviesRepository.cs: -------------------------------------------------------------------------------- 1 | using Movies.API.Entities; 2 | 3 | namespace Movies.API.Services; 4 | 5 | public interface IMoviesRepository 6 | { 7 | Task GetMovieAsync(Guid movieId); 8 | 9 | Task> GetMoviesAsync(); 10 | 11 | IAsyncEnumerable GetMoviesAsAsyncEnumerable(); 12 | 13 | void UpdateMovie(Movie movieToUpdate); 14 | 15 | void AddMovie(Movie movieToAdd); 16 | 17 | void DeleteMovie(Movie movieToDelete); 18 | 19 | Task SaveChangesAsync(); 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Accessing APIs with HttpClient in .NET 2 | Fully functioning sample code for my Accessing APIs with HttpClient in .NET course, currently targeting .NET 8. 3 | 4 | The **main** branch exactly matches the course. The **latest-and-greatest** branch contains changes that were incorporated after recording. Most often these changes are language features that are relatively new and/or in preview, like primary constructors, switch expressions and so on. Most of these changes will probably make it into the main branch when course updates happen, but if you don't want to wait for that you can already check it out - enjoy :-) 5 | 6 | All my courses can be found at https://app.pluralsight.com/profile/author/kevin-dockx 7 | -------------------------------------------------------------------------------- /Finished sample/Movies.Client/UnauthorizedApiAccessException.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace Movies.Client 4 | { 5 | [Serializable] 6 | public class UnauthorizedApiAccessException : Exception 7 | { 8 | public UnauthorizedApiAccessException() 9 | { 10 | } 11 | 12 | public UnauthorizedApiAccessException(string? message) : base(message) 13 | { 14 | } 15 | 16 | public UnauthorizedApiAccessException(string? message, Exception? innerException) : base(message, innerException) 17 | { 18 | } 19 | 20 | protected UnauthorizedApiAccessException(SerializationInfo info, StreamingContext context) : base(info, context) 21 | { 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Finished sample/Movies.Tests/TestableClassWithApiAccessUnitTests.cs: -------------------------------------------------------------------------------- 1 | using Movies.Client; 2 | using Movies.Client.Handlers; 3 | 4 | namespace Movies.Tests; 5 | 6 | public class TestableClassWithApiAccessUnitTests 7 | { 8 | [Fact] 9 | public async void GetMovie_On401Response_MustThrowUnauthorizedApiAccessException() 10 | { 11 | var httpClient = new HttpClient 12 | (new Return401UnauthorizedResponseHandler()) 13 | { 14 | BaseAddress = new Uri("http://localhost:5001") 15 | }; 16 | 17 | var testableClass = new TestableClassWithApiAccess( 18 | httpClient, 19 | new ()); 20 | 21 | await Assert.ThrowsAsync( 22 | () => testableClass.GetMovieAsync(CancellationToken.None)); 23 | } 24 | } -------------------------------------------------------------------------------- /Starter files/Movies.API/Profiles/MoviesProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace Movies.API; 4 | 5 | /// 6 | /// AutoMapper profile for working with Movie objects 7 | /// 8 | public class MoviesProfile : Profile 9 | { 10 | public MoviesProfile() 11 | { 12 | CreateMap() 13 | .ForMember(dest => dest.Director, opt => opt.MapFrom(src => 14 | $"{src.Director.FirstName} {src.Director.LastName}")) 15 | .ConstructUsing(src => new Models.Movie(src.Id, 16 | src.Title, 17 | src.ReleaseDate, 18 | src.Genre, 19 | src.Description, 20 | string.Empty)); 21 | 22 | CreateMap(); 23 | 24 | CreateMap().ReverseMap(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Profiles/MoviesProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace Movies.API; 4 | 5 | /// 6 | /// AutoMapper profile for working with Movie objects 7 | /// 8 | public class MoviesProfile : Profile 9 | { 10 | public MoviesProfile() 11 | { 12 | CreateMap() 13 | .ForMember(dest => dest.Director, opt => opt.MapFrom(src => 14 | $"{src.Director.FirstName} {src.Director.LastName}")) 15 | .ConstructUsing(src => new Models.Movie(src.Id, 16 | src.Title, 17 | src.ReleaseDate, 18 | src.Genre, 19 | src.Description, 20 | string.Empty)); 21 | 22 | CreateMap(); 23 | 24 | CreateMap().ReverseMap(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Movies.API": { 4 | "commandName": "Project", 5 | "launchUrl": "swagger", 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "dotnetRunMessages": true, 10 | "applicationUrl": "http://localhost:5001" 11 | }, 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | } 20 | }, 21 | "$schema": "https://json.schemastore.org/launchsettings.json", 22 | "iisSettings": { 23 | "windowsAuthentication": false, 24 | "anonymousAuthentication": true, 25 | "iisExpress": { 26 | "applicationUrl": "http://localhost:5001", 27 | "sslPort": 0 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Finished sample/Movies.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Movies.API": { 4 | "commandName": "Project", 5 | "launchUrl": "swagger", 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "dotnetRunMessages": true, 10 | "applicationUrl": "http://localhost:5001" 11 | }, 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | } 20 | }, 21 | "$schema": "https://json.schemastore.org/launchsettings.json", 22 | "iisSettings": { 23 | "windowsAuthentication": false, 24 | "anonymousAuthentication": true, 25 | "iisExpress": { 26 | "applicationUrl": "http://localhost:5001", 27 | "sslPort": 0 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Starter files/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 | public Poster(Guid id, 15 | Guid movieId, 16 | string name, 17 | byte[] bytes) 18 | { 19 | Id = id; 20 | MovieId = movieId; 21 | Name = name; 22 | Bytes = bytes; 23 | } 24 | 25 | #pragma warning disable CS8618 26 | // Parameterless constructor required for XML serialization 27 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 28 | public Poster() 29 | #pragma warning restore CS8618 30 | { 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Finished sample/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 | public Poster(Guid id, 15 | Guid movieId, 16 | string name, 17 | byte[] bytes) 18 | { 19 | Id = id; 20 | MovieId = movieId; 21 | Name = name; 22 | Bytes = bytes; 23 | } 24 | 25 | #pragma warning disable CS8618 26 | // Parameterless constructor required for XML serialization 27 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 28 | public Poster() 29 | #pragma warning restore CS8618 30 | { 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Models/PosterForCreation.cs: -------------------------------------------------------------------------------- 1 | using Movies.API.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Text; 6 | 7 | namespace Movies.API.Models; 8 | 9 | public class PosterForCreation 10 | { 11 | [Required] 12 | [MaxLength(200)] 13 | public string Name { get; set; } 14 | 15 | [Required] 16 | public byte[] Bytes { get; set; } 17 | 18 | public PosterForCreation( 19 | string name, 20 | byte[] bytes) 21 | { 22 | Name = name; 23 | Bytes = bytes; 24 | } 25 | 26 | #pragma warning disable CS8618 27 | // Parameterless constructor required for XML serialization 28 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 29 | public PosterForCreation() 30 | #pragma warning restore CS8618 31 | { 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Models/PosterForCreation.cs: -------------------------------------------------------------------------------- 1 | using Movies.API.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Text; 6 | 7 | namespace Movies.API.Models; 8 | 9 | public class PosterForCreation 10 | { 11 | [Required] 12 | [MaxLength(200)] 13 | public string Name { get; set; } 14 | 15 | [Required] 16 | public byte[] Bytes { get; set; } 17 | 18 | public PosterForCreation( 19 | string name, 20 | byte[] bytes) 21 | { 22 | Name = name; 23 | Bytes = bytes; 24 | } 25 | 26 | #pragma warning disable CS8618 27 | // Parameterless constructor required for XML serialization 28 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 29 | public PosterForCreation() 30 | #pragma warning restore CS8618 31 | { 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Models/Trailer.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.API.Models; 2 | 3 | public class Trailer 4 | { 5 | public Guid Id { get; set; } 6 | public Guid MovieId { get; set; } 7 | public string Name { get; set; } 8 | public string? Description { get; set; } 9 | public byte[] Bytes { get; set; } 10 | 11 | public Trailer(Guid id, 12 | Guid movieId, 13 | string name, 14 | string? description, 15 | byte[] bytes) 16 | { 17 | Id = id; 18 | MovieId = movieId; 19 | Name = name; 20 | Description = description; 21 | Bytes = bytes; 22 | } 23 | 24 | #pragma warning disable CS8618 25 | // Parameterless constructor required for XML serialization 26 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 27 | public Trailer() 28 | #pragma warning restore CS8618 29 | { 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Models/Trailer.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.API.Models; 2 | 3 | public class Trailer 4 | { 5 | public Guid Id { get; set; } 6 | public Guid MovieId { get; set; } 7 | public string Name { get; set; } 8 | public string? Description { get; set; } 9 | public byte[] Bytes { get; set; } 10 | 11 | public Trailer(Guid id, 12 | Guid movieId, 13 | string name, 14 | string? description, 15 | byte[] bytes) 16 | { 17 | Id = id; 18 | MovieId = movieId; 19 | Name = name; 20 | Description = description; 21 | Bytes = bytes; 22 | } 23 | 24 | #pragma warning disable CS8618 25 | // Parameterless constructor required for XML serialization 26 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 27 | public Trailer() 28 | #pragma warning restore CS8618 29 | { 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/InternalModels/Poster.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Movies.API.InternalModels; 4 | 5 | public class Poster 6 | { 7 | [Required] 8 | public Guid Id { get; set; } 9 | 10 | [Required] 11 | public Guid MovieId { get; set; } 12 | 13 | [Required] 14 | [MaxLength(200)] 15 | public string Name { get; set; } 16 | 17 | [Required] 18 | public byte[] Bytes { get; set; } 19 | 20 | public Poster(Guid id, 21 | Guid movieId, 22 | string name, 23 | byte[] bytes) 24 | { 25 | Id = id; 26 | MovieId = movieId; 27 | Name = name; 28 | Bytes = bytes; 29 | } 30 | 31 | #pragma warning disable CS8618 32 | // Parameterless constructor required for XML serialization 33 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 34 | public Poster() 35 | #pragma warning restore CS8618 36 | { 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Starter files/Movies.API/InternalModels/Poster.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Movies.API.InternalModels; 4 | 5 | public class Poster 6 | { 7 | [Required] 8 | public Guid Id { get; set; } 9 | 10 | [Required] 11 | public Guid MovieId { get; set; } 12 | 13 | [Required] 14 | [MaxLength(200)] 15 | public string Name { get; set; } 16 | 17 | [Required] 18 | public byte[] Bytes { get; set; } 19 | 20 | public Poster(Guid id, 21 | Guid movieId, 22 | string name, 23 | byte[] bytes) 24 | { 25 | Id = id; 26 | MovieId = movieId; 27 | Name = name; 28 | Bytes = bytes; 29 | } 30 | 31 | #pragma warning disable CS8618 32 | // Parameterless constructor required for XML serialization 33 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 34 | public Poster() 35 | #pragma warning restore CS8618 36 | { 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Models/TrailerForCreation.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Movies.API.Models; 4 | 5 | public class TrailerForCreation 6 | { 7 | [Required] 8 | public Guid MovieId { get; set; } 9 | 10 | [Required] 11 | [MaxLength(200)] 12 | public string Name { get; set; } 13 | 14 | [MaxLength(1000)] 15 | public string? Description { get; set; } 16 | 17 | [Required] 18 | public byte[] Bytes { get; set; } 19 | 20 | public TrailerForCreation(Guid movieId, 21 | string name, 22 | string? description, 23 | byte[] bytes) 24 | { 25 | MovieId = movieId; 26 | Name = name; 27 | Description = description; 28 | Bytes = bytes; 29 | } 30 | 31 | #pragma warning disable CS8618 32 | // Parameterless constructor required for XML serialization 33 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 34 | public TrailerForCreation() 35 | #pragma warning restore CS8618 36 | { 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Models/TrailerForCreation.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Movies.API.Models; 4 | 5 | public class TrailerForCreation 6 | { 7 | [Required] 8 | public Guid MovieId { get; set; } 9 | 10 | [Required] 11 | [MaxLength(200)] 12 | public string Name { get; set; } 13 | 14 | [MaxLength(1000)] 15 | public string? Description { get; set; } 16 | 17 | [Required] 18 | public byte[] Bytes { get; set; } 19 | 20 | public TrailerForCreation(Guid movieId, 21 | string name, 22 | string? description, 23 | byte[] bytes) 24 | { 25 | MovieId = movieId; 26 | Name = name; 27 | Description = description; 28 | Bytes = bytes; 29 | } 30 | 31 | #pragma warning disable CS8618 32 | // Parameterless constructor required for XML serialization 33 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 34 | public TrailerForCreation() 35 | #pragma warning restore CS8618 36 | { 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Models/Movie.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.API.Models; 2 | 3 | public class Movie 4 | { 5 | public Guid Id { get; set; } 6 | public string Title { get; set; } 7 | public string? Description { get; set; } 8 | public string? Genre { get; set; } 9 | public DateTimeOffset ReleaseDate { get; set; } 10 | public string Director { get; set; } 11 | 12 | public Movie(Guid id, 13 | string title, 14 | DateTimeOffset releaseDate, 15 | string? genre, 16 | string? description, 17 | string director) 18 | { 19 | Id = id; 20 | Director = director; 21 | Title = title; 22 | Genre = genre; 23 | ReleaseDate = releaseDate; 24 | Description = description; 25 | } 26 | 27 | #pragma warning disable CS8618 28 | // Parameterless constructor required for XML serialization 29 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 30 | public Movie() 31 | #pragma warning restore CS8618 32 | { 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Models/Movie.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.API.Models; 2 | 3 | public class Movie 4 | { 5 | public Guid Id { get; set; } 6 | public string Title { get; set; } 7 | public string? Description { get; set; } 8 | public string? Genre { get; set; } 9 | public DateTimeOffset ReleaseDate { get; set; } 10 | public string Director { get; set; } 11 | 12 | public Movie(Guid id, 13 | string title, 14 | DateTimeOffset releaseDate, 15 | string? genre, 16 | string? description, 17 | string director) 18 | { 19 | Id = id; 20 | Director = director; 21 | Title = title; 22 | Genre = genre; 23 | ReleaseDate = releaseDate; 24 | Description = description; 25 | } 26 | 27 | #pragma warning disable CS8618 28 | // Parameterless constructor required for XML serialization 29 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 30 | public Movie() 31 | #pragma warning restore CS8618 32 | { 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Finished sample/Movies.Tests/Movies.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | all 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 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 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Movies.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Movies.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Starter files/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 | public Director(Guid id, 25 | string firstName, 26 | string lastName) 27 | { 28 | Id = id; 29 | FirstName = firstName; 30 | LastName = lastName; 31 | } 32 | 33 | #pragma warning disable CS8618 34 | // Parameterless constructor for easy AutoMapper integration 35 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 36 | public Director() 37 | #pragma warning restore CS8618 38 | { 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Finished sample/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 | public Director(Guid id, 25 | string firstName, 26 | string lastName) 27 | { 28 | Id = id; 29 | FirstName = firstName; 30 | LastName = lastName; 31 | } 32 | 33 | #pragma warning disable CS8618 34 | // Parameterless constructor for easy AutoMapper integration 35 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 36 | public Director() 37 | #pragma warning restore CS8618 38 | { 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Finished sample/Movies.Client/Handlers/RetryPolicyDelegatingHandler.cs: -------------------------------------------------------------------------------- 1 | namespace Movies.Client.Handlers; 2 | 3 | public class RetryPolicyDelegatingHandler : DelegatingHandler 4 | { 5 | private readonly int _maximumAmountOfRetries = 3; 6 | public RetryPolicyDelegatingHandler(int maximumAmountOfRetries) 7 | : base() 8 | { 9 | _maximumAmountOfRetries = maximumAmountOfRetries; 10 | } 11 | public RetryPolicyDelegatingHandler(HttpMessageHandler innerHandler, 12 | int maximumAmountOfRetries) 13 | : base(innerHandler) 14 | { 15 | _maximumAmountOfRetries = maximumAmountOfRetries; 16 | } 17 | 18 | protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 19 | { 20 | for (int i = 0; i < _maximumAmountOfRetries; i++) 21 | { 22 | var response = await base.SendAsync(request, cancellationToken); 23 | 24 | if (response.IsSuccessStatusCode) 25 | { 26 | return response; 27 | } 28 | } 29 | 30 | return await base.SendAsync(request, cancellationToken); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/InternalModels/Trailer.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Movies.API.InternalModels; 4 | 5 | public class Trailer 6 | { 7 | [Required] 8 | public Guid Id { get; set; } 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 | public Trailer(Guid id, 24 | Guid movieId, 25 | string name, 26 | byte[] bytes, 27 | string? description) 28 | { 29 | Id = id; 30 | MovieId = movieId; 31 | Name = name; 32 | Bytes = bytes; 33 | Description = description; 34 | } 35 | 36 | #pragma warning disable CS8618 37 | // Parameterless constructor required for XML serialization 38 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 39 | public Trailer() 40 | #pragma warning restore CS8618 41 | { 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Starter files/Movies.API/InternalModels/Trailer.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Movies.API.InternalModels; 4 | 5 | public class Trailer 6 | { 7 | [Required] 8 | public Guid Id { get; set; } 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 | public Trailer(Guid id, 24 | Guid movieId, 25 | string name, 26 | byte[] bytes, 27 | string? description) 28 | { 29 | Id = id; 30 | MovieId = movieId; 31 | Name = name; 32 | Bytes = bytes; 33 | Description = description; 34 | } 35 | 36 | #pragma warning disable CS8618 37 | // Parameterless constructor required for XML serialization 38 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 39 | public Trailer() 40 | #pragma warning restore CS8618 41 | { 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Models/MovieForCreation.cs: -------------------------------------------------------------------------------- 1 | using Movies.API.Entities; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace Movies.API.Models; 5 | 6 | public class MovieForCreation 7 | { 8 | [Required] 9 | [MaxLength(200)] 10 | public string Title { get; set; } 11 | 12 | [MaxLength(2000)] 13 | [MinLength(10)] 14 | public string? Description { get; set; } 15 | 16 | [MaxLength(200)] 17 | public string? Genre { get; set; } 18 | 19 | [Required] 20 | public DateTimeOffset ReleaseDate { get; set; } 21 | 22 | [Required] 23 | public Guid DirectorId { get; set; } 24 | 25 | public MovieForCreation(string title, 26 | DateTimeOffset releaseDate, 27 | Guid directorId, 28 | string? genre, 29 | string? description) 30 | { 31 | DirectorId = directorId; 32 | Title = title; 33 | Genre = genre; 34 | ReleaseDate = releaseDate; 35 | Description = description; 36 | } 37 | 38 | #pragma warning disable CS8618 39 | // Parameterless constructor required for XML serialization 40 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 41 | public MovieForCreation() 42 | #pragma warning restore CS8618 43 | { 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Models/MovieForCreation.cs: -------------------------------------------------------------------------------- 1 | using Movies.API.Entities; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace Movies.API.Models; 5 | 6 | public class MovieForCreation 7 | { 8 | [Required] 9 | [MaxLength(200)] 10 | public string Title { get; set; } 11 | 12 | [MaxLength(2000)] 13 | [MinLength(10)] 14 | public string? Description { get; set; } 15 | 16 | [MaxLength(200)] 17 | public string? Genre { get; set; } 18 | 19 | [Required] 20 | public DateTimeOffset ReleaseDate { get; set; } 21 | 22 | [Required] 23 | public Guid DirectorId { get; set; } 24 | 25 | public MovieForCreation(string title, 26 | DateTimeOffset releaseDate, 27 | Guid directorId, 28 | string? genre, 29 | string? description) 30 | { 31 | DirectorId = directorId; 32 | Title = title; 33 | Genre = genre; 34 | ReleaseDate = releaseDate; 35 | Description = description; 36 | } 37 | 38 | #pragma warning disable CS8618 39 | // Parameterless constructor required for XML serialization 40 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 41 | public MovieForCreation() 42 | #pragma warning restore CS8618 43 | { 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Finished sample/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 | public MovieForUpdate(string title, 27 | DateTimeOffset releaseDate, 28 | Guid directorId, 29 | string? genre, 30 | string? description) 31 | { 32 | DirectorId = directorId; 33 | Title = title; 34 | Genre = genre; 35 | ReleaseDate = releaseDate; 36 | Description = description; 37 | } 38 | #pragma warning disable CS8618 39 | // Parameterless constructor required for XML serialization 40 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 41 | public MovieForUpdate() 42 | #pragma warning restore CS8618 43 | { 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Starter files/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 | public MovieForUpdate(string title, 27 | DateTimeOffset releaseDate, 28 | Guid directorId, 29 | string? genre, 30 | string? description) 31 | { 32 | DirectorId = directorId; 33 | Title = title; 34 | Genre = genre; 35 | ReleaseDate = releaseDate; 36 | Description = description; 37 | } 38 | #pragma warning disable CS8618 39 | // Parameterless constructor required for XML serialization 40 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 41 | public MovieForUpdate() 42 | #pragma warning restore CS8618 43 | { 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Controllers/MoviesStreamController.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/moviesstream")] 14 | [ApiController] 15 | public class MoviesStreamController : ControllerBase 16 | { 17 | private readonly IMoviesRepository _moviesRepository; 18 | private readonly IMapper _mapper; 19 | 20 | public MoviesStreamController(IMoviesRepository moviesRepository, 21 | IMapper mapper) 22 | { 23 | _moviesRepository = moviesRepository ?? 24 | throw new ArgumentNullException(nameof(moviesRepository)); 25 | _mapper = mapper ?? 26 | throw new ArgumentNullException(nameof(mapper)); 27 | } 28 | 29 | [HttpGet] 30 | public async IAsyncEnumerable GetMoviesStream() 31 | { 32 | await foreach (var movie in 33 | _moviesRepository.GetMoviesAsAsyncEnumerable()) 34 | { 35 | // for demo purposes, add a delay to visually see the effect 36 | await Task.Delay(500); 37 | yield return _mapper.Map(movie); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Controllers/MoviesStreamController.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/moviesstream")] 14 | [ApiController] 15 | public class MoviesStreamController : ControllerBase 16 | { 17 | private readonly IMoviesRepository _moviesRepository; 18 | private readonly IMapper _mapper; 19 | 20 | public MoviesStreamController(IMoviesRepository moviesRepository, 21 | IMapper mapper) 22 | { 23 | _moviesRepository = moviesRepository ?? 24 | throw new ArgumentNullException(nameof(moviesRepository)); 25 | _mapper = mapper ?? 26 | throw new ArgumentNullException(nameof(mapper)); 27 | } 28 | 29 | [HttpGet] 30 | public async IAsyncEnumerable GetMoviesStream() 31 | { 32 | await foreach (var movie in 33 | _moviesRepository.GetMoviesAsAsyncEnumerable()) 34 | { 35 | // for demo purposes, add a delay to visually see the effect 36 | await Task.Delay(500); 37 | yield return _mapper.Map(movie); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Finished sample/Movies.Client/Movies.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Entities/Movie.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace Movies.API.Entities; 5 | 6 | [Table("Movies")] 7 | public class Movie 8 | { 9 | [Key] 10 | public Guid Id { get; set; } 11 | 12 | [Required] 13 | [MaxLength(200)] 14 | public string Title { get; set; } 15 | 16 | [MaxLength(2000)] 17 | public string? Description { get; set; } 18 | 19 | [MaxLength(200)] 20 | public string? Genre { get; set; } 21 | 22 | [Required] 23 | public DateTimeOffset ReleaseDate { get; set; } 24 | 25 | [Required] 26 | public Guid DirectorId { get; set; } 27 | public Director Director { get; set; } = null!; 28 | 29 | public Movie(Guid id, 30 | Guid directorId, 31 | string title, 32 | DateTimeOffset releaseDate, 33 | string? genre, 34 | string? description) 35 | { 36 | Id = id; 37 | DirectorId = directorId; 38 | Title = title; 39 | ReleaseDate = releaseDate; 40 | Genre = genre; 41 | Description = description; 42 | } 43 | 44 | #pragma warning disable CS8618 45 | // Parameterless constructor for easy AutoMapper integration 46 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 47 | public Movie() 48 | #pragma warning restore CS8618 49 | { 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Finished sample/Movies.Client/MoviesAPIClient.cs: -------------------------------------------------------------------------------- 1 | using Movies.Client.Helpers; 2 | using Movies.Client.Models; 3 | using System.Net.Http.Headers; 4 | using System.Text.Json; 5 | 6 | namespace Movies.Client; 7 | 8 | public class MoviesAPIClient 9 | { 10 | private HttpClient _client; 11 | private readonly JsonSerializerOptionsWrapper _jsonSerializerOptionsWrapper; 12 | 13 | public MoviesAPIClient(HttpClient client, 14 | JsonSerializerOptionsWrapper jsonSerializerOptionsWrapper) 15 | { 16 | _client = client; 17 | _jsonSerializerOptionsWrapper = jsonSerializerOptionsWrapper ?? 18 | throw new ArgumentNullException(nameof(jsonSerializerOptionsWrapper)); 19 | _client.BaseAddress = new Uri("http://localhost:5001"); 20 | _client.Timeout = new TimeSpan(0, 0, 30); 21 | } 22 | 23 | public async Task?> GetMoviesAsync() 24 | { 25 | var request = new HttpRequestMessage( 26 | HttpMethod.Get, 27 | "api/movies"); 28 | request.Headers.Accept.Add( 29 | new MediaTypeWithQualityHeaderValue("application/json")); 30 | 31 | var response = await _client.SendAsync(request); 32 | response.EnsureSuccessStatusCode(); 33 | 34 | var content = await response.Content.ReadAsStringAsync(); 35 | 36 | return JsonSerializer.Deserialize>(content, 37 | _jsonSerializerOptionsWrapper.Options); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Entities/Movie.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace Movies.API.Entities; 5 | 6 | [Table("Movies")] 7 | public class Movie 8 | { 9 | [Key] 10 | public Guid Id { get; set; } 11 | 12 | [Required] 13 | [MaxLength(200)] 14 | public string Title { get; set; } 15 | 16 | [MaxLength(2000)] 17 | public string? Description { get; set; } 18 | 19 | [MaxLength(200)] 20 | public string? Genre { get; set; } 21 | 22 | [Required] 23 | public DateTimeOffset ReleaseDate { get; set; } 24 | 25 | [Required] 26 | public Guid DirectorId { get; set; } 27 | public Director Director { get; set; } = null!; 28 | 29 | public Movie(Guid id, 30 | Guid directorId, 31 | string title, 32 | DateTimeOffset releaseDate, 33 | string? genre, 34 | string? description) 35 | { 36 | Id = id; 37 | DirectorId = directorId; 38 | Title = title; 39 | ReleaseDate = releaseDate; 40 | Genre = genre; 41 | Description = description; 42 | } 43 | 44 | #pragma warning disable CS8618 45 | // Parameterless constructor for easy AutoMapper integration 46 | // Disabled warning: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 47 | public Movie() 48 | #pragma warning restore CS8618 49 | { 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Services/PostersRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Movies.API.DbContexts; 3 | using Movies.API.InternalModels; 4 | 5 | namespace Movies.API.Services; 6 | 7 | public class PostersRepository : IPostersRepository 8 | { 9 | private readonly MoviesDbContext _context; 10 | 11 | public PostersRepository(MoviesDbContext context) 12 | { 13 | _context = context ?? 14 | throw new ArgumentNullException(nameof(context)); 15 | } 16 | 17 | public async Task GetPosterAsync(Guid movieId, Guid posterId) 18 | { 19 | // Generate the name from the movie title. 20 | var movie = await _context.Movies 21 | .FirstOrDefaultAsync(m => m.Id == movieId); 22 | 23 | if (movie == null) 24 | { 25 | throw new Exception($"Movie with id {movieId} not found."); 26 | } 27 | 28 | // generate a movie poster of 500KB 29 | var random = new Random(); 30 | var generatedBytes = new byte[524288]; 31 | random.NextBytes(generatedBytes); 32 | 33 | return new Poster(posterId, 34 | movieId, 35 | $"{movie.Title} poster number {DateTime.UtcNow.Ticks}", 36 | generatedBytes); 37 | } 38 | 39 | public async Task AddPoster(Guid movieId, Poster posterToAdd) 40 | { 41 | // don't do anything: we're just faking this. Simply return the poster 42 | // after setting the ids 43 | posterToAdd.MovieId = movieId; 44 | posterToAdd.Id = Guid.NewGuid(); 45 | return await Task.FromResult(posterToAdd); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Services/PostersRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Movies.API.DbContexts; 3 | using Movies.API.InternalModels; 4 | 5 | namespace Movies.API.Services; 6 | 7 | public class PostersRepository : IPostersRepository 8 | { 9 | private readonly MoviesDbContext _context; 10 | 11 | public PostersRepository(MoviesDbContext context) 12 | { 13 | _context = context ?? 14 | throw new ArgumentNullException(nameof(context)); 15 | } 16 | 17 | public async Task GetPosterAsync(Guid movieId, Guid posterId) 18 | { 19 | // Generate the name from the movie title. 20 | var movie = await _context.Movies 21 | .FirstOrDefaultAsync(m => m.Id == movieId); 22 | 23 | if (movie == null) 24 | { 25 | throw new Exception($"Movie with id {movieId} not found."); 26 | } 27 | 28 | // generate a movie poster of 500KB 29 | var random = new Random(); 30 | var generatedBytes = new byte[524288]; 31 | random.NextBytes(generatedBytes); 32 | 33 | return new Poster(posterId, 34 | movieId, 35 | $"{movie.Title} poster number {DateTime.UtcNow.Ticks}", 36 | generatedBytes); 37 | } 38 | 39 | public async Task AddPoster(Guid movieId, Poster posterToAdd) 40 | { 41 | // don't do anything: we're just faking this. Simply return the poster 42 | // after setting the ids 43 | posterToAdd.MovieId = movieId; 44 | posterToAdd.Id = Guid.NewGuid(); 45 | return await Task.FromResult(posterToAdd); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Services/TrailersRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Movies.API.DbContexts; 3 | using Movies.API.InternalModels; 4 | 5 | namespace Movies.API.Services; 6 | 7 | public class TrailersRepository : ITrailersRepository 8 | { 9 | private MoviesDbContext _context; 10 | 11 | public TrailersRepository(MoviesDbContext context) 12 | { 13 | _context = context ?? 14 | throw new ArgumentNullException(nameof(context)); 15 | 16 | } 17 | 18 | public async Task GetTrailerAsync(Guid movieId, Guid trailerId) 19 | { 20 | // Generate the name from the movie title. 21 | var movie = await _context.Movies 22 | .FirstOrDefaultAsync(m => m.Id == movieId); 23 | 24 | if (movie == null) 25 | { 26 | throw new Exception($"Movie with id {movieId} not found."); 27 | } 28 | 29 | // generate a trailer (byte array) between 50 and 100MB 30 | var random = new Random(); 31 | var generatedByteLength = random.Next(52428800, 104857600); 32 | var generatedBytes = new byte[generatedByteLength]; 33 | random.NextBytes(generatedBytes); 34 | 35 | return new Trailer( 36 | trailerId, 37 | movieId, 38 | $"{movie.Title} trailer number {DateTime.UtcNow.Ticks}", 39 | generatedBytes, 40 | $"{movie.Title} trailer description {DateTime.UtcNow.Ticks}"); 41 | } 42 | 43 | public async Task AddTrailer(Guid movieId, Trailer trailerToAdd) 44 | { 45 | // don't do anything: we're just faking this. Simply return the trailer 46 | // after setting the ids 47 | trailerToAdd.MovieId = movieId; 48 | trailerToAdd.Id = Guid.NewGuid(); 49 | return await Task.FromResult(trailerToAdd); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Services/TrailersRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Movies.API.DbContexts; 3 | using Movies.API.InternalModels; 4 | 5 | namespace Movies.API.Services; 6 | 7 | public class TrailersRepository : ITrailersRepository 8 | { 9 | private MoviesDbContext _context; 10 | 11 | public TrailersRepository(MoviesDbContext context) 12 | { 13 | _context = context ?? 14 | throw new ArgumentNullException(nameof(context)); 15 | 16 | } 17 | 18 | public async Task GetTrailerAsync(Guid movieId, Guid trailerId) 19 | { 20 | // Generate the name from the movie title. 21 | var movie = await _context.Movies 22 | .FirstOrDefaultAsync(m => m.Id == movieId); 23 | 24 | if (movie == null) 25 | { 26 | throw new Exception($"Movie with id {movieId} not found."); 27 | } 28 | 29 | // generate a trailer (byte array) between 50 and 100MB 30 | var random = new Random(); 31 | var generatedByteLength = random.Next(52428800, 104857600); 32 | var generatedBytes = new byte[generatedByteLength]; 33 | random.NextBytes(generatedBytes); 34 | 35 | return new Trailer( 36 | trailerId, 37 | movieId, 38 | $"{movie.Title} trailer number {DateTime.UtcNow.Ticks}", 39 | generatedBytes, 40 | $"{movie.Title} trailer description {DateTime.UtcNow.Ticks}"); 41 | } 42 | 43 | public async Task AddTrailer(Guid movieId, Trailer trailerToAdd) 44 | { 45 | // don't do anything: we're just faking this. Simply return the trailer 46 | // after setting the ids 47 | trailerToAdd.MovieId = movieId; 48 | trailerToAdd.Id = Guid.NewGuid(); 49 | return await Task.FromResult(trailerToAdd); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Starter files/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 ?? 23 | throw new ArgumentNullException(nameof(postersRepository)); 24 | _mapper = mapper ?? 25 | throw new ArgumentNullException(nameof(mapper)); 26 | } 27 | 28 | [HttpGet("{posterId}", Name = "GetPoster")] 29 | public async Task> GetPoster(Guid movieId, 30 | Guid posterId) 31 | { 32 | var poster = await _postersRepository.GetPosterAsync(movieId, posterId); 33 | if (poster == null) 34 | { 35 | return NotFound(); 36 | } 37 | 38 | return Ok(_mapper.Map(poster)); 39 | } 40 | 41 | [HttpPost] 42 | public async Task CreatePoster(Guid movieId, 43 | [FromBody] Models.PosterForCreation posterForCreation) 44 | { 45 | var poster = _mapper.Map(posterForCreation); 46 | var createdPoster = await _postersRepository.AddPoster(movieId, poster); 47 | 48 | // no need to save, in this type of repo the poster is 49 | // immediately persisted. 50 | 51 | // map the poster from the repository to a shared model poster 52 | return CreatedAtRoute("GetPoster", 53 | new { movieId, posterId = createdPoster.Id }, 54 | _mapper.Map(createdPoster)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Finished sample/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 ?? 23 | throw new ArgumentNullException(nameof(postersRepository)); 24 | _mapper = mapper ?? 25 | throw new ArgumentNullException(nameof(mapper)); 26 | } 27 | 28 | [HttpGet("{posterId}", Name = "GetPoster")] 29 | public async Task> GetPoster(Guid movieId, 30 | Guid posterId) 31 | { 32 | var poster = await _postersRepository.GetPosterAsync(movieId, posterId); 33 | if (poster == null) 34 | { 35 | return NotFound(); 36 | } 37 | 38 | return Ok(_mapper.Map(poster)); 39 | } 40 | 41 | [HttpPost] 42 | public async Task CreatePoster(Guid movieId, 43 | [FromBody] Models.PosterForCreation posterForCreation) 44 | { 45 | var poster = _mapper.Map(posterForCreation); 46 | var createdPoster = await _postersRepository.AddPoster(movieId, poster); 47 | 48 | // no need to save, in this type of repo the poster is 49 | // immediately persisted. 50 | 51 | // map the poster from the repository to a shared model poster 52 | return CreatedAtRoute("GetPoster", 53 | new { movieId, posterId = createdPoster.Id }, 54 | _mapper.Map(createdPoster)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Services/MoviesRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Movies.API.DbContexts; 3 | using Movies.API.Entities; 4 | 5 | namespace Movies.API.Services; 6 | 7 | public class MoviesRepository : IMoviesRepository 8 | { 9 | private MoviesDbContext _context; 10 | 11 | public MoviesRepository(MoviesDbContext context) 12 | { 13 | _context = context ?? 14 | throw new ArgumentNullException(nameof(context)); 15 | } 16 | 17 | public async Task GetMovieAsync(Guid movieId) 18 | { 19 | return await _context.Movies.Include(m => m.Director) 20 | .FirstOrDefaultAsync(m => m.Id == movieId); 21 | } 22 | 23 | public async Task> GetMoviesAsync() 24 | { 25 | return await _context.Movies 26 | .Include(m => m.Director).ToListAsync(); 27 | } 28 | 29 | public IAsyncEnumerable GetMoviesAsAsyncEnumerable() 30 | { 31 | return _context.Movies.AsAsyncEnumerable(); 32 | } 33 | 34 | public void UpdateMovie(Movie movieToUpdate) 35 | { 36 | // no code required, entity tracked by context. Including 37 | // this is best practice to ensure other implementations of the 38 | // contract (eg a mock version) can execute code on update 39 | // when needed. 40 | } 41 | 42 | public void AddMovie(Movie movieToAdd) 43 | { 44 | if (movieToAdd == null) 45 | { 46 | throw new ArgumentNullException(nameof(movieToAdd)); 47 | } 48 | 49 | _context.Add(movieToAdd); 50 | } 51 | 52 | public void DeleteMovie(Movie movieToDelete) 53 | { 54 | if (movieToDelete == null) 55 | { 56 | throw new ArgumentNullException(nameof(movieToDelete)); 57 | } 58 | 59 | _context.Remove(movieToDelete); 60 | } 61 | 62 | public async Task SaveChangesAsync() 63 | { 64 | // return true if 1 or more entities were changed 65 | return (await _context.SaveChangesAsync() > 0); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Services/MoviesRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Movies.API.DbContexts; 3 | using Movies.API.Entities; 4 | 5 | namespace Movies.API.Services; 6 | 7 | public class MoviesRepository : IMoviesRepository 8 | { 9 | private MoviesDbContext _context; 10 | 11 | public MoviesRepository(MoviesDbContext context) 12 | { 13 | _context = context ?? 14 | throw new ArgumentNullException(nameof(context)); 15 | } 16 | 17 | public async Task GetMovieAsync(Guid movieId) 18 | { 19 | return await _context.Movies.Include(m => m.Director) 20 | .FirstOrDefaultAsync(m => m.Id == movieId); 21 | } 22 | 23 | public async Task> GetMoviesAsync() 24 | { 25 | return await _context.Movies 26 | .Include(m => m.Director).ToListAsync(); 27 | } 28 | 29 | public IAsyncEnumerable GetMoviesAsAsyncEnumerable() 30 | { 31 | return _context.Movies.AsAsyncEnumerable(); 32 | } 33 | 34 | public void UpdateMovie(Movie movieToUpdate) 35 | { 36 | // no code required, entity tracked by context. Including 37 | // this is best practice to ensure other implementations of the 38 | // contract (eg a mock version) can execute code on update 39 | // when needed. 40 | } 41 | 42 | public void AddMovie(Movie movieToAdd) 43 | { 44 | if (movieToAdd == null) 45 | { 46 | throw new ArgumentNullException(nameof(movieToAdd)); 47 | } 48 | 49 | _context.Add(movieToAdd); 50 | } 51 | 52 | public void DeleteMovie(Movie movieToDelete) 53 | { 54 | if (movieToDelete == null) 55 | { 56 | throw new ArgumentNullException(nameof(movieToDelete)); 57 | } 58 | 59 | _context.Remove(movieToDelete); 60 | } 61 | 62 | public async Task SaveChangesAsync() 63 | { 64 | // return true if 1 or more entities were changed 65 | return (await _context.SaveChangesAsync() > 0); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Finished sample/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 ?? 23 | throw new ArgumentNullException(nameof(trailersRepository)); 24 | _mapper = mapper ?? 25 | throw new ArgumentNullException(nameof(mapper)); 26 | } 27 | 28 | [HttpGet("{trailerId}", Name = "GetTrailer")] 29 | public async Task> GetTrailer(Guid movieId, 30 | Guid trailerId) 31 | { 32 | var trailer = await _trailersRepository.GetTrailerAsync(movieId, trailerId); 33 | if (trailer == null) 34 | { 35 | return NotFound(); 36 | } 37 | 38 | return Ok(_mapper.Map(trailer)); 39 | } 40 | 41 | [HttpPost] 42 | public async Task CreateTrailer(Guid movieId, 43 | [FromBody] Models.TrailerForCreation trailerForCreation) 44 | { 45 | var trailer = _mapper.Map(trailerForCreation); 46 | var createdTrailer = await _trailersRepository.AddTrailer(movieId, trailer); 47 | 48 | // no need to save, in this type of repo the trailer is 49 | // immediately persisted. 50 | 51 | // map the trailer from the repository to a shared model trailer 52 | return CreatedAtRoute("GetTrailer", 53 | new { movieId, trailerId = createdTrailer.Id }, 54 | _mapper.Map(createdTrailer)); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Starter files/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 ?? 23 | throw new ArgumentNullException(nameof(trailersRepository)); 24 | _mapper = mapper ?? 25 | throw new ArgumentNullException(nameof(mapper)); 26 | } 27 | 28 | [HttpGet("{trailerId}", Name = "GetTrailer")] 29 | public async Task> GetTrailer(Guid movieId, 30 | Guid trailerId) 31 | { 32 | var trailer = await _trailersRepository.GetTrailerAsync(movieId, trailerId); 33 | if (trailer == null) 34 | { 35 | return NotFound(); 36 | } 37 | 38 | return Ok(_mapper.Map(trailer)); 39 | } 40 | 41 | [HttpPost] 42 | public async Task CreateTrailer(Guid movieId, 43 | [FromBody] Models.TrailerForCreation trailerForCreation) 44 | { 45 | var trailer = _mapper.Map(trailerForCreation); 46 | var createdTrailer = await _trailersRepository.AddTrailer(movieId, trailer); 47 | 48 | // no need to save, in this type of repo the trailer is 49 | // immediately persisted. 50 | 51 | // map the trailer from the repository to a shared model trailer 52 | return CreatedAtRoute("GetTrailer", 53 | new { movieId, trailerId = createdTrailer.Id }, 54 | _mapper.Map(createdTrailer)); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Starter files/Movies.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32922.545 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Movies.API", "Movies.API\Movies.API.csproj", "{DB2E81EB-634D-4288-A27A-E6AFEAC9046E}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "01 - Client", "01 - Client", "{0115C779-EBF1-491D-BD29-E68E538A1830}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "02 - Server", "02 - Server", "{EED9B125-54EB-4C0F-AB0D-507B09E2F220}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Movies.Client", "Movies.Client\Movies.Client.csproj", "{9C97614C-AD56-45B8-8E5F-3ABEAB36A6E0}" 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 | {DB2E81EB-634D-4288-A27A-E6AFEAC9046E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {DB2E81EB-634D-4288-A27A-E6AFEAC9046E}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {DB2E81EB-634D-4288-A27A-E6AFEAC9046E}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {DB2E81EB-634D-4288-A27A-E6AFEAC9046E}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {9C97614C-AD56-45B8-8E5F-3ABEAB36A6E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {9C97614C-AD56-45B8-8E5F-3ABEAB36A6E0}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {9C97614C-AD56-45B8-8E5F-3ABEAB36A6E0}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {9C97614C-AD56-45B8-8E5F-3ABEAB36A6E0}.Release|Any CPU.Build.0 = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(SolutionProperties) = preSolution 30 | HideSolutionNode = FALSE 31 | EndGlobalSection 32 | GlobalSection(NestedProjects) = preSolution 33 | {DB2E81EB-634D-4288-A27A-E6AFEAC9046E} = {EED9B125-54EB-4C0F-AB0D-507B09E2F220} 34 | {9C97614C-AD56-45B8-8E5F-3ABEAB36A6E0} = {0115C779-EBF1-491D-BD29-E68E538A1830} 35 | EndGlobalSection 36 | GlobalSection(ExtensibilityGlobals) = postSolution 37 | SolutionGuid = {49B98022-5AAA-43D7-A13D-33A45D4CC577} 38 | EndGlobalSection 39 | EndGlobal 40 | -------------------------------------------------------------------------------- /Finished sample/Movies.Client/TestableClassWithApiAccess.cs: -------------------------------------------------------------------------------- 1 | using Movies.Client.Helpers; 2 | using Movies.Client.Models; 3 | using System.Net.Http.Headers; 4 | using System.Text.Json; 5 | 6 | namespace Movies.Client; 7 | 8 | public class TestableClassWithApiAccess 9 | { 10 | private readonly HttpClient _httpClient; 11 | private readonly JsonSerializerOptionsWrapper _jsonSerializerOptionsWrapper; 12 | 13 | public TestableClassWithApiAccess(HttpClient httpClient, 14 | JsonSerializerOptionsWrapper jsonSerializerOptionsWrapper) 15 | { 16 | _httpClient = httpClient; 17 | _jsonSerializerOptionsWrapper = jsonSerializerOptionsWrapper; 18 | } 19 | 20 | public async Task GetMovieAsync( 21 | CancellationToken cancellationToken) 22 | { 23 | var request = new HttpRequestMessage( 24 | HttpMethod.Get, 25 | "api/movies/030a43b0-f9a5-405a-811c-bf342524b2be"); 26 | request.Headers.Accept.Add( 27 | new MediaTypeWithQualityHeaderValue("application/json")); 28 | request.Headers.AcceptEncoding.Add( 29 | new StringWithQualityHeaderValue("gzip")); 30 | 31 | using (var response = await _httpClient.SendAsync(request, 32 | HttpCompletionOption.ResponseHeadersRead, 33 | cancellationToken)) 34 | { 35 | if (!response.IsSuccessStatusCode) 36 | { 37 | // inspect the status code 38 | if (response.StatusCode == System.Net.HttpStatusCode.NotFound) 39 | { 40 | // show this to the user 41 | Console.WriteLine("The requested movie cannot be found."); 42 | return null; 43 | } 44 | else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) 45 | { 46 | // trigger a login flow 47 | throw new UnauthorizedApiAccessException(); 48 | } 49 | 50 | response.EnsureSuccessStatusCode(); 51 | 52 | var stream = await response.Content.ReadAsStreamAsync(); 53 | return await JsonSerializer.DeserializeAsync( 54 | stream, 55 | _jsonSerializerOptionsWrapper.Options); 56 | } 57 | } 58 | return null; 59 | } 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Finished sample/Movies.Client/Services/RemoteStreamingSamples.cs: -------------------------------------------------------------------------------- 1 | using Movies.Client.Helpers; 2 | using Movies.Client.Models; 3 | using System.Net.Http.Headers; 4 | using System.Net.Http.Json; 5 | using System.Text; 6 | using System.Text.Json; 7 | 8 | namespace Movies.Client.Services; 9 | 10 | public class RemoteStreamingSamples : IIntegrationService 11 | { 12 | private readonly IHttpClientFactory _httpClientFactory; 13 | private readonly JsonSerializerOptionsWrapper _jsonSerializerOptionsWrapper; 14 | 15 | public RemoteStreamingSamples(IHttpClientFactory httpClientFactory, 16 | JsonSerializerOptionsWrapper jsonSerializerOptionsWrapper) 17 | { 18 | _jsonSerializerOptionsWrapper = jsonSerializerOptionsWrapper ?? 19 | throw new ArgumentNullException(nameof(jsonSerializerOptionsWrapper)); 20 | _httpClientFactory = httpClientFactory ?? 21 | throw new ArgumentNullException(nameof(httpClientFactory)); 22 | } 23 | public async Task RunAsync() 24 | { 25 | await GetStreamingMoviesAsync(); 26 | } 27 | 28 | private async Task GetStreamingMoviesAsync() 29 | { 30 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 31 | 32 | var request = new HttpRequestMessage( 33 | HttpMethod.Get, 34 | $"api/moviesstream"); 35 | request.Headers.Accept.Add( 36 | new MediaTypeWithQualityHeaderValue("application/json")); 37 | 38 | var response = await httpClient.SendAsync(request, 39 | HttpCompletionOption.ResponseHeadersRead); 40 | response.EnsureSuccessStatusCode(); 41 | 42 | // regular deserialization 43 | //var content = await response.Content.ReadAsStringAsync(); 44 | //var movies = JsonSerializer.Deserialize>( 45 | // content, 46 | // _jsonSerializerOptionsWrapper.Options); 47 | 48 | var responseStream = await response.Content.ReadAsStreamAsync(); 49 | var movies = JsonSerializer.DeserializeAsyncEnumerable( 50 | responseStream, 51 | _jsonSerializerOptionsWrapper.Options); 52 | 53 | await foreach (var movie in movies) 54 | { 55 | Console.WriteLine(movie?.Title); 56 | } 57 | 58 | 59 | //foreach (var movie in movies) 60 | //{ 61 | // Console.WriteLine(movie?.Title); 62 | //} 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /Finished sample/Movies.Client/Services/CustomMessageHandlersSamples.cs: -------------------------------------------------------------------------------- 1 | using Movies.Client.Helpers; 2 | using System.Net.Http.Headers; 3 | using System.Net; 4 | using System.Text.Json; 5 | using Movies.Client.Models; 6 | 7 | namespace Movies.Client.Services; 8 | 9 | public class CustomMessageHandlersSamples : IIntegrationService 10 | { 11 | private readonly IHttpClientFactory _httpClientFactory; 12 | private readonly JsonSerializerOptionsWrapper _jsonSerializerOptionsWrapper; 13 | 14 | public CustomMessageHandlersSamples(IHttpClientFactory httpClientFactory, 15 | JsonSerializerOptionsWrapper jsonSerializerOptionsWrapper) 16 | { 17 | _jsonSerializerOptionsWrapper = jsonSerializerOptionsWrapper ?? 18 | throw new ArgumentNullException(nameof(jsonSerializerOptionsWrapper)); 19 | _httpClientFactory = httpClientFactory ?? 20 | throw new ArgumentNullException(nameof(httpClientFactory)); 21 | } 22 | 23 | public async Task RunAsync() 24 | { 25 | await GetMovieWithCustomRetryHandlerAsync( 26 | CancellationToken.None); 27 | } 28 | 29 | public async Task GetMovieWithCustomRetryHandlerAsync(CancellationToken cancellationToken) 30 | { 31 | var httpClient = _httpClientFactory 32 | .CreateClient("MoviesAPIClientWithCustomHandler"); 33 | 34 | var request = new HttpRequestMessage( 35 | HttpMethod.Get, 36 | "api/movies/030a43b0-f9a5-405a-811c-bf342524b2be"); 37 | request.Headers.Accept.Add( 38 | new MediaTypeWithQualityHeaderValue("application/json")); 39 | request.Headers.AcceptEncoding.Add( 40 | new StringWithQualityHeaderValue("gzip")); 41 | 42 | using (var response = await httpClient.SendAsync(request, 43 | HttpCompletionOption.ResponseHeadersRead, 44 | cancellationToken)) 45 | { 46 | if (!response.IsSuccessStatusCode) 47 | { 48 | // inspect the status code 49 | if (response.StatusCode == HttpStatusCode.NotFound) 50 | { 51 | // show this to the user 52 | Console.WriteLine("The requested movie cannot be found."); 53 | return; 54 | } 55 | 56 | response.EnsureSuccessStatusCode(); 57 | } 58 | 59 | var stream = await response.Content.ReadAsStreamAsync(); 60 | var movie = await JsonSerializer.DeserializeAsync( 61 | stream, 62 | _jsonSerializerOptionsWrapper.Options); 63 | 64 | } 65 | 66 | } 67 | } -------------------------------------------------------------------------------- /Starter files/Movies.Client/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | using Movies.Client.Services; 5 | 6 | using IHost host = Host.CreateDefaultBuilder(args) 7 | .ConfigureServices((_, services) => 8 | { 9 | // register services for DI 10 | services.AddLogging(configure => configure.AddDebug().AddConsole()); 11 | 12 | // For the cancellation samples 13 | // services.AddScoped(); 14 | 15 | // For the compression samples 16 | // services.AddScoped(); 17 | 18 | // For the CRUD samples 19 | services.AddScoped(); 20 | 21 | // For the compression samples 22 | // services.AddScoped(); 23 | 24 | // For the custom message handler samples 25 | // services.AddScoped(); 26 | 27 | // For the faults and errors samples 28 | // services.AddScoped(); 29 | 30 | // For the HttpClientFactory samples 31 | // services.AddScoped(); 32 | 33 | // For the local streams samples 34 | // services.AddScoped(); 35 | 36 | // For the partial update samples 37 | // services.AddScoped(); 38 | 39 | // For the remote streaming samples 40 | // services.AddScoped(); 41 | 42 | }).Build(); 43 | 44 | 45 | 46 | // For demo purposes: overall catch-all to log any exception that might 47 | // happen to the console & wait for key input afterwards so we can easily 48 | // inspect the issue. 49 | try 50 | { 51 | var logger = host.Services.GetRequiredService>(); 52 | logger.LogInformation("Host created."); 53 | 54 | // Run the IntegrationService containing all samples and 55 | // await this call to ensure the application doesn't 56 | // prematurely exit. 57 | await host.Services.GetRequiredService().RunAsync(); 58 | } 59 | catch (Exception generalException) 60 | { 61 | // log the exception 62 | var logger = host.Services.GetRequiredService>(); 63 | logger.LogError(generalException, 64 | "An exception happened while running the integration service."); 65 | } 66 | 67 | Console.ReadKey(); 68 | 69 | await host.RunAsync(); 70 | 71 | -------------------------------------------------------------------------------- /Finished sample/Movies.Client/Services/PartialUpdateSamples.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.JsonPatch; 2 | using Movies.Client.Models; 3 | using Newtonsoft.Json; 4 | using System.Net.Http.Headers; 5 | using System.Text; 6 | 7 | namespace Movies.Client.Services; 8 | 9 | public class PartialUpdateSamples : IIntegrationService 10 | { 11 | private readonly IHttpClientFactory _httpClientFactory; 12 | 13 | public PartialUpdateSamples(IHttpClientFactory httpClientFactory) 14 | { 15 | _httpClientFactory = httpClientFactory ?? 16 | throw new ArgumentNullException(nameof(httpClientFactory)); 17 | } 18 | public async Task RunAsync() 19 | { 20 | // await PatchResourceAsync(); 21 | await PatchResourceShortcutAsync(); 22 | } 23 | 24 | public async Task PatchResourceAsync() 25 | { 26 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 27 | 28 | var patchDoc = new JsonPatchDocument(); 29 | patchDoc.Replace(m => m.Title, "Updated title"); 30 | patchDoc.Remove(m => m.Description); 31 | 32 | var serializedChangeSet = JsonConvert.SerializeObject(patchDoc); 33 | var request = new HttpRequestMessage( 34 | HttpMethod.Patch, 35 | "api/movies/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"); 36 | request.Headers.Accept.Add( 37 | new MediaTypeWithQualityHeaderValue("application/json")); 38 | 39 | request.Content = new StringContent(serializedChangeSet); 40 | request.Content.Headers.ContentType = 41 | new MediaTypeHeaderValue("application/json-patch+json"); 42 | 43 | var response = await httpClient.SendAsync(request); 44 | response.EnsureSuccessStatusCode(); 45 | 46 | var content = await response.Content.ReadAsStringAsync(); 47 | var updatedMovie = JsonConvert.DeserializeObject(content); 48 | } 49 | 50 | public async Task PatchResourceShortcutAsync() 51 | { 52 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 53 | 54 | var patchDoc = new JsonPatchDocument(); 55 | patchDoc.Replace(m => m.Title, "Updated title"); 56 | patchDoc.Remove(m => m.Description); 57 | 58 | var response = await httpClient.PatchAsync( 59 | "api/movies/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", 60 | new StringContent( 61 | JsonConvert.SerializeObject(patchDoc), 62 | Encoding.UTF8, 63 | "application/json-patch+json")); 64 | 65 | response.EnsureSuccessStatusCode(); 66 | 67 | var content = await response.Content.ReadAsStringAsync(); 68 | var updatedMovie = JsonConvert.DeserializeObject(content); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Movies.API.DbContexts; 3 | using Movies.API.Services; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | 7 | // Add services to the container. 8 | 9 | builder.Services.AddControllers(options => 10 | { 11 | // Return a 406 when an unsupported media type was requested 12 | options.ReturnHttpNotAcceptable = true; 13 | }) 14 | // Override System.Text.Json with Json.NET 15 | //.AddNewtonsoftJson(setupAction => 16 | //{ 17 | // setupAction.SerializerSettings.ContractResolver = 18 | // new CamelCasePropertyNamesContractResolver(); 19 | //}); 20 | .AddXmlSerializerFormatters(); 21 | 22 | // add support for (de)compressing requests/responses (eg gzip) 23 | builder.Services.AddResponseCompression(); 24 | builder.Services.AddRequestDecompression(); 25 | 26 | // register the DbContext on the container, getting the 27 | // connection string from appSettings 28 | builder.Services.AddDbContext(o => o.UseSqlite( 29 | builder.Configuration["ConnectionStrings:MoviesDBConnectionString"])); 30 | 31 | builder.Services.AddScoped(); 32 | builder.Services.AddScoped(); 33 | builder.Services.AddScoped(); 34 | 35 | builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); 36 | 37 | 38 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle 39 | builder.Services.AddEndpointsApiExplorer(); 40 | builder.Services.AddSwaggerGen(setupAction => 41 | { 42 | setupAction.SwaggerDoc("v1", 43 | new() { Title = "Movies API", Version = "v1" }); 44 | } 45 | ); 46 | 47 | var app = builder.Build(); 48 | 49 | // For demo purposes, delete the database & migrate on startup so 50 | // we can start with a clean slate 51 | using (var scope = app.Services.CreateScope()) 52 | { 53 | try 54 | { 55 | var context = scope.ServiceProvider.GetService(); 56 | if (context != null) 57 | { 58 | await context.Database.EnsureDeletedAsync(); 59 | await context.Database.MigrateAsync(); 60 | } 61 | } 62 | catch (Exception ex) 63 | { 64 | var logger = scope.ServiceProvider.GetRequiredService(); 65 | logger.LogError(ex, "An error occurred while migrating the database."); 66 | } 67 | } 68 | 69 | // Configure the HTTP request pipeline. 70 | if (app.Environment.IsDevelopment()) 71 | { 72 | app.UseSwagger(); 73 | app.UseSwaggerUI(); 74 | } 75 | 76 | // use response compression (client should pass through 77 | // Accept-Encoding) 78 | app.UseResponseCompression(); 79 | 80 | // use request decompression (client should pass through 81 | // Content-Encoding) 82 | app.UseRequestDecompression(); 83 | 84 | app.UseAuthorization(); 85 | 86 | app.MapControllers(); 87 | 88 | app.Run(); 89 | -------------------------------------------------------------------------------- /Finished sample/Movies.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32922.545 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Movies.API", "Movies.API\Movies.API.csproj", "{DB2E81EB-634D-4288-A27A-E6AFEAC9046E}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "01 - Client", "01 - Client", "{0115C779-EBF1-491D-BD29-E68E538A1830}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "02 - Server", "02 - Server", "{EED9B125-54EB-4C0F-AB0D-507B09E2F220}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Movies.Client", "Movies.Client\Movies.Client.csproj", "{9C97614C-AD56-45B8-8E5F-3ABEAB36A6E0}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "03 - Unit Tests", "03 - Unit Tests", "{493FD44C-24BC-48C3-85CD-CADDD9943B60}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Movies.Tests", "Movies.Tests\Movies.Tests.csproj", "{FAB79310-5F55-418F-A479-1BE5789D12A1}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {DB2E81EB-634D-4288-A27A-E6AFEAC9046E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {DB2E81EB-634D-4288-A27A-E6AFEAC9046E}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {DB2E81EB-634D-4288-A27A-E6AFEAC9046E}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {DB2E81EB-634D-4288-A27A-E6AFEAC9046E}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {9C97614C-AD56-45B8-8E5F-3ABEAB36A6E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {9C97614C-AD56-45B8-8E5F-3ABEAB36A6E0}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {9C97614C-AD56-45B8-8E5F-3ABEAB36A6E0}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {9C97614C-AD56-45B8-8E5F-3ABEAB36A6E0}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {FAB79310-5F55-418F-A479-1BE5789D12A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {FAB79310-5F55-418F-A479-1BE5789D12A1}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {FAB79310-5F55-418F-A479-1BE5789D12A1}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {FAB79310-5F55-418F-A479-1BE5789D12A1}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(NestedProjects) = preSolution 41 | {DB2E81EB-634D-4288-A27A-E6AFEAC9046E} = {EED9B125-54EB-4C0F-AB0D-507B09E2F220} 42 | {9C97614C-AD56-45B8-8E5F-3ABEAB36A6E0} = {0115C779-EBF1-491D-BD29-E68E538A1830} 43 | {FAB79310-5F55-418F-A479-1BE5789D12A1} = {493FD44C-24BC-48C3-85CD-CADDD9943B60} 44 | EndGlobalSection 45 | GlobalSection(ExtensibilityGlobals) = postSolution 46 | SolutionGuid = {49B98022-5AAA-43D7-A13D-33A45D4CC577} 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Formatters; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.OpenApi.Models; 4 | using Movies.API.DbContexts; 5 | using Movies.API.Services; 6 | using Newtonsoft.Json.Serialization; 7 | 8 | var builder = WebApplication.CreateBuilder(args); 9 | 10 | // Add services to the container. 11 | 12 | builder.Services.AddControllers(options => 13 | { 14 | // Return a 406 when an unsupported media type was requested 15 | options.ReturnHttpNotAcceptable = true; 16 | 17 | //options.OutputFormatters.Insert(0, new XmlSerializerOutputFormatter()); 18 | //options.InputFormatters.Insert(0, new XmlSerializerInputFormatter(options)); 19 | }) 20 | // Override System.Text.Json with Json.NET 21 | //.AddNewtonsoftJson(setupAction => 22 | //{ 23 | // setupAction.SerializerSettings.ContractResolver = 24 | // new CamelCasePropertyNamesContractResolver(); 25 | //}) 26 | .AddXmlSerializerFormatters(); 27 | 28 | // add support for (de)compressing requests/responses (eg gzip) 29 | builder.Services.AddResponseCompression(); 30 | builder.Services.AddRequestDecompression(); 31 | 32 | // register the DbContext on the container, getting the 33 | // connection string from appSettings 34 | builder.Services.AddDbContext(o => o.UseSqlite( 35 | builder.Configuration["ConnectionStrings:MoviesDBConnectionString"])); 36 | 37 | builder.Services.AddScoped(); 38 | builder.Services.AddScoped(); 39 | builder.Services.AddScoped(); 40 | 41 | builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); 42 | 43 | 44 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle 45 | builder.Services.AddEndpointsApiExplorer(); 46 | builder.Services.AddSwaggerGen(setupAction => 47 | { 48 | setupAction.SwaggerDoc("v1", 49 | new() { Title = "Movies API", Version = "v1" }); 50 | } 51 | ); 52 | 53 | var app = builder.Build(); 54 | 55 | // For demo purposes, delete the database & migrate on startup so 56 | // we can start with a clean slate 57 | using (var scope = app.Services.CreateScope()) 58 | { 59 | try 60 | { 61 | var context = scope.ServiceProvider.GetService(); 62 | if (context != null) 63 | { 64 | await context.Database.EnsureDeletedAsync(); 65 | await context.Database.MigrateAsync(); 66 | } 67 | } 68 | catch (Exception ex) 69 | { 70 | var logger = scope.ServiceProvider.GetRequiredService(); 71 | logger.LogError(ex, "An error occurred while migrating the database."); 72 | } 73 | } 74 | 75 | // Configure the HTTP request pipeline. 76 | if (app.Environment.IsDevelopment()) 77 | { 78 | app.UseSwagger(); 79 | app.UseSwaggerUI(); 80 | } 81 | 82 | // use response compression (client should pass through 83 | // Accept-Encoding) 84 | app.UseResponseCompression(); 85 | 86 | // use request decompression (client should pass through 87 | // Content-Encoding) 88 | app.UseRequestDecompression(); 89 | 90 | app.UseAuthorization(); 91 | 92 | app.MapControllers(); 93 | 94 | app.Run(); 95 | -------------------------------------------------------------------------------- /Finished sample/Movies.Client/Services/CancellationSamples.cs: -------------------------------------------------------------------------------- 1 | using Movies.Client.Helpers; 2 | using Movies.Client.Models; 3 | using System.Net.Http.Headers; 4 | using System.Text.Json; 5 | 6 | namespace Movies.Client.Services; 7 | 8 | public class CancellationSamples : IIntegrationService 9 | { 10 | private readonly IHttpClientFactory _httpClientFactory; 11 | private readonly JsonSerializerOptionsWrapper _jsonSerializerOptionsWrapper; 12 | private CancellationTokenSource _cancellationTokenSource = 13 | new CancellationTokenSource(); 14 | 15 | public CancellationSamples(IHttpClientFactory httpClientFactory, 16 | JsonSerializerOptionsWrapper jsonSerializerOptionsWrapper) 17 | { 18 | _jsonSerializerOptionsWrapper = jsonSerializerOptionsWrapper ?? 19 | throw new ArgumentNullException(nameof(jsonSerializerOptionsWrapper)); 20 | _httpClientFactory = httpClientFactory ?? 21 | throw new ArgumentNullException(nameof(httpClientFactory)); 22 | } 23 | 24 | 25 | public async Task RunAsync() 26 | { 27 | _cancellationTokenSource.CancelAfter(200); 28 | // await GetTrailerAndCancelAsync(_cancellationTokenSource.Token); 29 | await GetTrailerAndHandleTimeoutAsync(); 30 | } 31 | 32 | private async Task GetTrailerAndCancelAsync(CancellationToken cancellationToken) 33 | { 34 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 35 | 36 | var request = new HttpRequestMessage( 37 | HttpMethod.Get, 38 | $"api/movies/d8663e5e-7494-4f81-8739-6e0de1bea7ee/trailers/{Guid.NewGuid()}"); 39 | request.Headers.Accept.Add( 40 | new MediaTypeWithQualityHeaderValue("application/json")); 41 | request.Headers.AcceptEncoding.Add( 42 | new StringWithQualityHeaderValue("gzip")); 43 | 44 | try 45 | { 46 | using (var response = await httpClient.SendAsync(request, 47 | HttpCompletionOption.ResponseHeadersRead, cancellationToken)) 48 | { 49 | var stream = await response.Content.ReadAsStreamAsync(); 50 | 51 | response.EnsureSuccessStatusCode(); 52 | 53 | var poster = await JsonSerializer.DeserializeAsync( 54 | stream, 55 | _jsonSerializerOptionsWrapper.Options); 56 | } 57 | } 58 | catch (OperationCanceledException ocException) 59 | { 60 | Console.WriteLine($"An operation was cancelled with message {ocException.Message}."); 61 | // additional cleanup, ... 62 | } 63 | } 64 | 65 | private async Task GetTrailerAndHandleTimeoutAsync() 66 | { 67 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 68 | 69 | var request = new HttpRequestMessage( 70 | HttpMethod.Get, 71 | $"api/movies/d8663e5e-7494-4f81-8739-6e0de1bea7ee/trailers/{Guid.NewGuid()}"); 72 | request.Headers.Accept.Add( 73 | new MediaTypeWithQualityHeaderValue("application/json")); 74 | request.Headers.AcceptEncoding.Add( 75 | 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 | 86 | var poster = await JsonSerializer.DeserializeAsync( 87 | stream, 88 | _jsonSerializerOptionsWrapper.Options); 89 | } 90 | } 91 | catch (OperationCanceledException ocException) 92 | { 93 | Console.WriteLine($"An operation was cancelled with message {ocException.Message}."); 94 | // additional cleanup, ... 95 | } 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /Finished sample/Movies.Client/Services/HttpClientFactorySamples.cs: -------------------------------------------------------------------------------- 1 | using Movies.Client.Helpers; 2 | using Movies.Client.Models; 3 | using System.Net.Http.Headers; 4 | using System.Text.Json; 5 | 6 | namespace Movies.Client.Services; 7 | 8 | public class HttpClientFactorySamples : IIntegrationService 9 | { 10 | private readonly IHttpClientFactory _httpClientFactory; 11 | private readonly JsonSerializerOptionsWrapper _jsonSerializerOptionsWrapper; 12 | private readonly MoviesAPIClient _moviesAPIClient; 13 | 14 | public HttpClientFactorySamples(IHttpClientFactory httpClientFactory, 15 | JsonSerializerOptionsWrapper jsonSerializerOptionsWrapper, 16 | MoviesAPIClient moviesAPIClient) 17 | { 18 | _jsonSerializerOptionsWrapper = jsonSerializerOptionsWrapper ?? 19 | throw new ArgumentNullException(nameof(jsonSerializerOptionsWrapper)); 20 | _moviesAPIClient = moviesAPIClient ?? 21 | throw new ArgumentNullException(nameof(moviesAPIClient)); 22 | _httpClientFactory = httpClientFactory ?? 23 | throw new ArgumentNullException(nameof(httpClientFactory)); 24 | } 25 | 26 | public async Task RunAsync() 27 | { 28 | // await TestDisposeHttpClientAsync(); 29 | // await TestReuseHttpClientAsync(); 30 | // await GetFilmsAsync(); 31 | // await GetMoviesWithTypedHttpClientAsync(); 32 | await GetMoviesViaMoviesAPIClientAsync(); 33 | } 34 | 35 | public async Task GetFilmsAsync() 36 | { 37 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 38 | 39 | var request = new HttpRequestMessage( 40 | HttpMethod.Get, 41 | "api/films"); 42 | request.Headers.Accept.Add( 43 | new MediaTypeWithQualityHeaderValue("application/json")); 44 | 45 | var response = await httpClient.SendAsync(request); 46 | response.EnsureSuccessStatusCode(); 47 | 48 | var content = await response.Content.ReadAsStringAsync(); 49 | 50 | var movies = JsonSerializer.Deserialize>( 51 | content, 52 | _jsonSerializerOptionsWrapper.Options); 53 | } 54 | 55 | private async Task GetMoviesViaMoviesAPIClientAsync() 56 | { 57 | var movies = await _moviesAPIClient.GetMoviesAsync(); 58 | } 59 | 60 | 61 | //private async Task GetMoviesWithTypedHttpClientAsync() 62 | //{ 63 | // var request = new HttpRequestMessage( 64 | // HttpMethod.Get, 65 | // "api/movies"); 66 | // request.Headers.Accept.Add( 67 | // new MediaTypeWithQualityHeaderValue("application/json")); 68 | 69 | // var response = await _moviesAPIClient.Client.SendAsync(request); 70 | // response.EnsureSuccessStatusCode(); 71 | 72 | // var content = await response.Content.ReadAsStringAsync(); 73 | 74 | // var movies = JsonSerializer.Deserialize>(content, 75 | // _jsonSerializerOptionsWrapper.Options); 76 | //} 77 | 78 | 79 | private async Task TestDisposeHttpClientAsync() 80 | { 81 | for (var i = 0; i < 10; i++) 82 | { 83 | using (var httpClient = new HttpClient()) 84 | { 85 | var request = new HttpRequestMessage( 86 | HttpMethod.Get, 87 | "https://www.google.com"); 88 | 89 | var response = await httpClient.SendAsync(request); 90 | response.EnsureSuccessStatusCode(); 91 | 92 | var content = await response.Content.ReadAsStringAsync(); 93 | Console.WriteLine($"Request completed with status code " + 94 | $"{response.StatusCode}"); 95 | } 96 | } 97 | } 98 | 99 | private async Task TestReuseHttpClientAsync() 100 | { 101 | var httpClient = new HttpClient(); 102 | 103 | for (int i = 0; i < 10; i++) 104 | { 105 | var request = new HttpRequestMessage( 106 | HttpMethod.Get, 107 | "https://www.google.com"); 108 | 109 | var response = await httpClient.SendAsync(request); 110 | response.EnsureSuccessStatusCode(); 111 | 112 | var content = await response.Content.ReadAsStringAsync(); 113 | Console.WriteLine($"Request completed with status code " + 114 | $"{response.StatusCode}"); 115 | } 116 | } 117 | 118 | 119 | } 120 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/DbContexts/MoviesDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Movies.API.Entities; 3 | 4 | namespace Movies.API.DbContexts; 5 | 6 | public class MoviesDbContext : DbContext 7 | { 8 | public DbSet Movies { get; set; } = null!; 9 | 10 | public MoviesDbContext(DbContextOptions options) 11 | : base(options) 12 | { 13 | } 14 | 15 | // seed the database with data 16 | protected override void OnModelCreating(ModelBuilder modelBuilder) 17 | { 18 | modelBuilder.Entity().HasData( 19 | new(Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 20 | "Quentin", 21 | "Tarantino"), 22 | new(Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 23 | "Joel", 24 | "Coen"), 25 | new(Guid.Parse("c19099ed-94db-44ba-885b-0ad7205d5e40"), 26 | "Martin", 27 | "Scorsese"), 28 | new(Guid.Parse("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), 29 | "David", 30 | "Fincher"), 31 | new(Guid.Parse("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), 32 | "Bryan", 33 | "Singer"), 34 | new(Guid.Parse("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), 35 | "James", 36 | "Cameron")); 37 | 38 | modelBuilder.Entity().HasData( 39 | new(Guid.Parse("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 40 | Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 41 | "Pulp Fiction", 42 | new DateTimeOffset(new DateTime(1994, 11, 9)), 43 | "Crime, Drama", 44 | "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."), 45 | new(Guid.Parse("6e87f657-f2c1-4d90-9b37-cbe43cc6adb9"), 46 | Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 47 | "Jackie Brown", 48 | new DateTimeOffset(new DateTime(1997, 12, 25)), 49 | "Crime, Drama", 50 | "A middle-aged woman finds herself in the middle of a huge conflict that will either make her a profit or cost her life."), 51 | new(Guid.Parse("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 52 | Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 53 | "The Big Lebowski", 54 | new DateTimeOffset(new DateTime(1998, 3, 6)), 55 | "Comedy, Crime", 56 | "The Dude (Lebowski), mistaken for a millionaire Lebowski, seeks restitution for his ruined rug and enlists his bowling buddies to help get it."), 57 | new(Guid.Parse("f9a16fee-4c49-41bb-87a1-bbaad0cd1174"), 58 | Guid.Parse("c19099ed-94db-44ba-885b-0ad7205d5e40"), 59 | "Casino", 60 | new DateTimeOffset(new DateTime(1995, 11, 22)), 61 | "Crime, Drama", 62 | "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."), 63 | new(Guid.Parse("bb6a100a-053f-4bf8-b271-60ce3aae6eb5"), 64 | Guid.Parse("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), 65 | "Fight Club", 66 | new DateTimeOffset(new DateTime(1999, 10, 15)), 67 | "Drama", 68 | "An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more."), 69 | new(Guid.Parse("3d2880ae-5ba6-417c-845d-f4ebfd4bcac7"), 70 | Guid.Parse("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), 71 | "The Usual Suspects", 72 | new DateTimeOffset(new DateTime(1995, 9, 15)), 73 | "Crime, Thriller", 74 | "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."), 75 | new(Guid.Parse("26fcbcc4-b7f7-47fc-9382-740c12246b59"), 76 | Guid.Parse("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), 77 | "Terminator 2: Judgment Day", 78 | new DateTimeOffset(new DateTime(1991, 7, 3)), 79 | "Action, Sci-Fi", 80 | "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.")); 81 | 82 | base.OnModelCreating(modelBuilder); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Starter files/Movies.API/DbContexts/MoviesDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Movies.API.Entities; 3 | 4 | namespace Movies.API.DbContexts; 5 | 6 | public class MoviesDbContext : DbContext 7 | { 8 | public DbSet Movies { get; set; } = null!; 9 | 10 | public MoviesDbContext(DbContextOptions options) 11 | : base(options) 12 | { 13 | } 14 | 15 | // seed the database with data 16 | protected override void OnModelCreating(ModelBuilder modelBuilder) 17 | { 18 | modelBuilder.Entity().HasData( 19 | new(Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 20 | "Quentin", 21 | "Tarantino"), 22 | new(Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 23 | "Joel", 24 | "Coen"), 25 | new(Guid.Parse("c19099ed-94db-44ba-885b-0ad7205d5e40"), 26 | "Martin", 27 | "Scorsese"), 28 | new(Guid.Parse("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), 29 | "David", 30 | "Fincher"), 31 | new(Guid.Parse("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), 32 | "Bryan", 33 | "Singer"), 34 | new(Guid.Parse("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), 35 | "James", 36 | "Cameron")); 37 | 38 | modelBuilder.Entity().HasData( 39 | new(Guid.Parse("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 40 | Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 41 | "Pulp Fiction", 42 | new DateTimeOffset(new DateTime(1994, 11, 9)), 43 | "Crime, Drama", 44 | "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."), 45 | new(Guid.Parse("6e87f657-f2c1-4d90-9b37-cbe43cc6adb9"), 46 | Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 47 | "Jackie Brown", 48 | new DateTimeOffset(new DateTime(1997, 12, 25)), 49 | "Crime, Drama", 50 | "A middle-aged woman finds herself in the middle of a huge conflict that will either make her a profit or cost her life."), 51 | new(Guid.Parse("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 52 | Guid.Parse("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 53 | "The Big Lebowski", 54 | new DateTimeOffset(new DateTime(1998, 3, 6)), 55 | "Comedy, Crime", 56 | "The Dude (Lebowski), mistaken for a millionaire Lebowski, seeks restitution for his ruined rug and enlists his bowling buddies to help get it."), 57 | new(Guid.Parse("f9a16fee-4c49-41bb-87a1-bbaad0cd1174"), 58 | Guid.Parse("c19099ed-94db-44ba-885b-0ad7205d5e40"), 59 | "Casino", 60 | new DateTimeOffset(new DateTime(1995, 11, 22)), 61 | "Crime, Drama", 62 | "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."), 63 | new(Guid.Parse("bb6a100a-053f-4bf8-b271-60ce3aae6eb5"), 64 | Guid.Parse("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), 65 | "Fight Club", 66 | new DateTimeOffset(new DateTime(1999, 10, 15)), 67 | "Drama", 68 | "An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more."), 69 | new(Guid.Parse("3d2880ae-5ba6-417c-845d-f4ebfd4bcac7"), 70 | Guid.Parse("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), 71 | "The Usual Suspects", 72 | new DateTimeOffset(new DateTime(1995, 9, 15)), 73 | "Crime, Thriller", 74 | "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."), 75 | new(Guid.Parse("26fcbcc4-b7f7-47fc-9382-740c12246b59"), 76 | Guid.Parse("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), 77 | "Terminator 2: Judgment Day", 78 | new DateTimeOffset(new DateTime(1991, 7, 3)), 79 | "Action, Sci-Fi", 80 | "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.")); 81 | 82 | base.OnModelCreating(modelBuilder); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Finished sample/Movies.Client/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | using Movies.Client; 5 | using Movies.Client.Handlers; 6 | using Movies.Client.Helpers; 7 | using Movies.Client.Services; 8 | using Polly; 9 | 10 | using IHost host = Host.CreateDefaultBuilder(args) 11 | .ConfigureServices((_, services) => 12 | { 13 | // register services for DI 14 | services.AddLogging(configure => configure.AddDebug().AddConsole()); 15 | 16 | services.AddSingleton(); 17 | 18 | services.AddTransient(fact => 19 | { 20 | return new RetryPolicyDelegatingHandler(2); 21 | }); 22 | 23 | services.AddHttpClient("MoviesAPIClientWithCustomHandler", configureClient => 24 | { 25 | configureClient.BaseAddress = new Uri("http://localhost:5001"); 26 | configureClient.Timeout = new TimeSpan(0, 0, 30); 27 | }) 28 | .AddHttpMessageHandler() 29 | //.AddHttpMessageHandler(() => 30 | //{ 31 | // return new RetryPolicyDelegatingHandler(2); 32 | //}) 33 | .ConfigurePrimaryHttpMessageHandler(() => 34 | { 35 | var handler = new SocketsHttpHandler(); 36 | handler.AutomaticDecompression = System.Net.DecompressionMethods.GZip; 37 | return handler; 38 | }); 39 | 40 | services.AddHttpClient("MoviesAPIClient", 41 | configureClient => 42 | { 43 | configureClient.BaseAddress = new Uri("http://localhost:5001"); 44 | configureClient.Timeout = new TimeSpan(0, 0, 30); 45 | }).AddPolicyHandler(Policy.HandleResult( 46 | response => !response.IsSuccessStatusCode) 47 | .RetryAsync(5)) 48 | 49 | .ConfigurePrimaryHttpMessageHandler(() => 50 | { 51 | var handler = new SocketsHttpHandler(); 52 | handler.AutomaticDecompression = System.Net.DecompressionMethods.GZip; 53 | //handler.AllowAutoRedirect = false; 54 | return handler; 55 | }); 56 | 57 | services.AddHttpClient(configureClient => 58 | { 59 | //configureClient.BaseAddress = new Uri("http://localhost:5001"); 60 | //configureClient.Timeout = new TimeSpan(0, 0, 30); 61 | }); 62 | 63 | 64 | // For the cancellation samples 65 | //services.AddScoped(); 66 | 67 | // For the compression samples 68 | // services.AddScoped(); 69 | 70 | // For the CRUD samples 71 | // services.AddScoped(); 72 | 73 | // For the compression samples 74 | // services.AddScoped(); 75 | 76 | // For the custom message handler samples 77 | services.AddScoped(); 78 | 79 | // For the faults and errors samples 80 | // services.AddScoped(); 81 | 82 | // For the HttpClientFactory samples 83 | // services.AddScoped(); 84 | 85 | // For the local streams samples 86 | // services.AddScoped(); 87 | 88 | // For the partial update samples 89 | // services.AddScoped(); 90 | 91 | // For the remote streaming samples 92 | //services.AddScoped(); 93 | 94 | }).Build(); 95 | 96 | 97 | 98 | // For demo purposes: overall catch-all to log any exception that might 99 | // happen to the console & wait for key input afterwards so we can easily 100 | // inspect the issue. 101 | try 102 | { 103 | var logger = host.Services.GetRequiredService>(); 104 | logger.LogInformation("Host created."); 105 | 106 | // Run the IntegrationService containing all samples and 107 | // await this call to ensure the application doesn't 108 | // prematurely exit. 109 | await host.Services.GetRequiredService().RunAsync(); 110 | } 111 | catch (Exception generalException) 112 | { 113 | // log the exception 114 | var logger = host.Services.GetRequiredService>(); 115 | logger.LogError(generalException, 116 | "An exception happened while running the integration service."); 117 | } 118 | 119 | Console.ReadKey(); 120 | 121 | await host.RunAsync(); 122 | 123 | -------------------------------------------------------------------------------- /Finished sample/Movies.Client/Services/CompressionSamples.cs: -------------------------------------------------------------------------------- 1 | using Movies.Client.Helpers; 2 | using Movies.Client.Models; 3 | using System.IO.Compression; 4 | using System.Net.Http.Headers; 5 | using System.Text.Json; 6 | 7 | namespace Movies.Client.Services; 8 | 9 | public class CompressionSamples : IIntegrationService 10 | { 11 | private readonly IHttpClientFactory _httpClientFactory; 12 | private readonly JsonSerializerOptionsWrapper _jsonSerializerOptionsWrapper; 13 | 14 | public CompressionSamples(IHttpClientFactory httpClientFactory, 15 | JsonSerializerOptionsWrapper jsonSerializerOptionsWrapper) 16 | { 17 | _jsonSerializerOptionsWrapper = jsonSerializerOptionsWrapper ?? 18 | throw new ArgumentNullException(nameof(jsonSerializerOptionsWrapper)); 19 | _httpClientFactory = httpClientFactory ?? 20 | throw new ArgumentNullException(nameof(httpClientFactory)); 21 | } 22 | public async Task RunAsync() 23 | { 24 | // await GetPosterWithGZipCompressionAsync(); 25 | await SendAndReceivePosterWithGZipCompressionAsync(); 26 | } 27 | 28 | private async Task GetPosterWithGZipCompressionAsync() 29 | { 30 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 31 | 32 | var request = new HttpRequestMessage( 33 | HttpMethod.Get, 34 | $"api/movies/d8663e5e-7494-4f81-8739-6e0de1bea7ee/posters/{Guid.NewGuid()}"); 35 | request.Headers.Accept.Add( 36 | new MediaTypeWithQualityHeaderValue("application/json")); 37 | request.Headers.AcceptEncoding.Add( 38 | new StringWithQualityHeaderValue("gzip")); 39 | 40 | using (var response = await httpClient.SendAsync(request, 41 | HttpCompletionOption.ResponseHeadersRead)) 42 | { 43 | var stream = await response.Content.ReadAsStreamAsync(); 44 | 45 | response.EnsureSuccessStatusCode(); 46 | 47 | var poster = await JsonSerializer.DeserializeAsync( 48 | stream, 49 | _jsonSerializerOptionsWrapper.Options); 50 | } 51 | } 52 | 53 | private async Task SendAndReceivePosterWithGZipCompressionAsync() 54 | { 55 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 56 | 57 | // generate a movie poster of 5MB 58 | var random = new Random(); 59 | var generatedBytes = new byte[5242880]; 60 | random.NextBytes(generatedBytes); 61 | 62 | var posterForCreation = new PosterForCreation() 63 | { 64 | Name = "A new poster for The Big Lebowski", 65 | Bytes = generatedBytes 66 | }; 67 | 68 | using (var memoryContentStream = new MemoryStream()) 69 | { 70 | await JsonSerializer.SerializeAsync( 71 | memoryContentStream, 72 | posterForCreation); 73 | 74 | memoryContentStream.Seek(0, SeekOrigin.Begin); 75 | 76 | using (var request = new HttpRequestMessage( 77 | HttpMethod.Post, 78 | "api/movies/d8663e5e-7494-4f81-8739-6e0de1bea7ee/posters")) 79 | { 80 | request.Headers.Accept.Add( 81 | new MediaTypeWithQualityHeaderValue("application/json")); 82 | request.Headers.AcceptEncoding.Add( 83 | new StringWithQualityHeaderValue("gzip")); 84 | 85 | using (var compressedMemoryContentStream = new MemoryStream()) 86 | { 87 | using (var gzipStream = new GZipStream( 88 | compressedMemoryContentStream, 89 | CompressionMode.Compress)) 90 | { 91 | memoryContentStream.CopyTo(gzipStream); 92 | gzipStream.Flush(); 93 | compressedMemoryContentStream.Position = 0; 94 | 95 | using (var streamContent = new StreamContent(compressedMemoryContentStream)) 96 | { 97 | streamContent.Headers.ContentType = 98 | new MediaTypeHeaderValue("application/json"); 99 | streamContent.Headers.ContentEncoding.Add("gzip"); 100 | 101 | request.Content = streamContent; 102 | 103 | var response = await httpClient.SendAsync(request, 104 | HttpCompletionOption.ResponseHeadersRead); 105 | response.EnsureSuccessStatusCode(); 106 | 107 | var stream = await response.Content.ReadAsStreamAsync(); 108 | var poster = await JsonSerializer.DeserializeAsync( 109 | stream, 110 | _jsonSerializerOptionsWrapper.Options); 111 | 112 | // do something with the newly created poster 113 | 114 | } 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Controllers/MoviesController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.AspNetCore.JsonPatch; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Movies.API.Entities; 5 | using Movies.API.Services; 6 | 7 | namespace Movies.API.Controllers; 8 | 9 | [Route("api/movies")] 10 | [ApiController] 11 | public class MoviesController : ControllerBase 12 | { 13 | private readonly IMoviesRepository _moviesRepository; 14 | private readonly IMapper _mapper; 15 | 16 | public MoviesController(IMoviesRepository moviesRepository, 17 | IMapper mapper) 18 | { 19 | _moviesRepository = moviesRepository ?? 20 | throw new ArgumentNullException(nameof(moviesRepository)); 21 | _mapper = mapper ?? 22 | throw new ArgumentNullException(nameof(mapper)); 23 | } 24 | 25 | [HttpGet(Name = "GetMovies")] 26 | public async Task>> GetMovies() 27 | { 28 | var movieEntities = await _moviesRepository.GetMoviesAsync(); 29 | var mapped = _mapper.Map>(movieEntities); 30 | return Ok(_mapper.Map>(movieEntities)); 31 | } 32 | 33 | 34 | [HttpGet("{movieId}", Name = "GetMovie")] 35 | public async Task> GetMovie(Guid movieId) 36 | { 37 | var movieEntity = await _moviesRepository.GetMovieAsync(movieId); 38 | if (movieEntity == null) 39 | { 40 | return NotFound(); 41 | } 42 | 43 | return Ok(_mapper.Map(movieEntity)); 44 | } 45 | 46 | [HttpPost] 47 | public async Task CreateMovie( 48 | [FromBody] Models.MovieForCreation movieForCreation) 49 | { 50 | var movieEntity = _mapper.Map(movieForCreation); 51 | _moviesRepository.AddMovie(movieEntity); 52 | 53 | // save the changes 54 | await _moviesRepository.SaveChangesAsync(); 55 | 56 | // Fetch the movie from the data store so the director is included 57 | await _moviesRepository.GetMovieAsync(movieEntity.Id); 58 | 59 | return CreatedAtRoute("GetMovie", 60 | new { movieId = movieEntity.Id }, 61 | _mapper.Map(movieEntity)); 62 | } 63 | 64 | [HttpPut("{movieId}")] 65 | public async Task UpdateMovie(Guid movieId, 66 | [FromBody] Models.MovieForUpdate movieForUpdate) 67 | { 68 | var movieEntity = await _moviesRepository.GetMovieAsync(movieId); 69 | if (movieEntity == null) 70 | { 71 | return NotFound(); 72 | } 73 | 74 | // map the inputted object into the movie entity 75 | // this ensures properties will get updated 76 | _mapper.Map(movieForUpdate, movieEntity); 77 | 78 | // call into UpdateMovie even though in our implementation 79 | // this doesn't contain code - doing this ensures the code stays 80 | // reliable when other repository implemenations (eg: a mock 81 | // repository) are used. 82 | _moviesRepository.UpdateMovie(movieEntity); 83 | 84 | await _moviesRepository.SaveChangesAsync(); 85 | 86 | // return the updated movie, after mapping it 87 | return Ok(_mapper.Map(movieEntity)); 88 | } 89 | 90 | [HttpPatch("{movieId}")] 91 | public async Task PartiallyUpdateMovie(Guid movieId, 92 | [FromBody] JsonPatchDocument patchDoc) 93 | { 94 | var movieEntity = await _moviesRepository.GetMovieAsync(movieId); 95 | if (movieEntity == null) 96 | { 97 | return NotFound(); 98 | } 99 | 100 | // the patch is on a DTO, not on the movie entity 101 | var movieToPatch = _mapper.Map(movieEntity); 102 | 103 | patchDoc.ApplyTo(movieToPatch, ModelState); 104 | 105 | if (!ModelState.IsValid) 106 | { 107 | return new UnprocessableEntityObjectResult(ModelState); 108 | } 109 | 110 | // map back to the entity, and save 111 | _mapper.Map(movieToPatch, movieEntity); 112 | 113 | // call into UpdateMovie even though in our implementation 114 | // this doesn't contain code - doing this ensures the code stays 115 | // reliable when other repository implemenations (eg: a mock 116 | // repository) are used. 117 | _moviesRepository.UpdateMovie(movieEntity); 118 | 119 | await _moviesRepository.SaveChangesAsync(); 120 | 121 | // return the updated movie, after mapping it 122 | return Ok(_mapper.Map(movieEntity)); 123 | } 124 | 125 | [HttpDelete("{movieid}")] 126 | public async Task DeleteMovie(Guid movieId) 127 | { 128 | var movieEntity = await _moviesRepository.GetMovieAsync(movieId); 129 | if (movieEntity == null) 130 | { 131 | return NotFound(); 132 | } 133 | 134 | _moviesRepository.DeleteMovie(movieEntity); 135 | await _moviesRepository.SaveChangesAsync(); 136 | 137 | return NoContent(); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Controllers/MoviesController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.AspNetCore.JsonPatch; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Movies.API.Entities; 5 | using Movies.API.Services; 6 | 7 | namespace Movies.API.Controllers; 8 | 9 | [Route("api/movies")] 10 | [ApiController] 11 | public class MoviesController : ControllerBase 12 | { 13 | private readonly IMoviesRepository _moviesRepository; 14 | private readonly IMapper _mapper; 15 | 16 | public MoviesController(IMoviesRepository moviesRepository, 17 | IMapper mapper) 18 | { 19 | _moviesRepository = moviesRepository ?? 20 | throw new ArgumentNullException(nameof(moviesRepository)); 21 | _mapper = mapper ?? 22 | throw new ArgumentNullException(nameof(mapper)); 23 | } 24 | 25 | [HttpGet(Name = "GetMovies")] 26 | public async Task>> GetMovies() 27 | { 28 | var movieEntities = await _moviesRepository.GetMoviesAsync(); 29 | var mapped = _mapper.Map>(movieEntities); 30 | return Ok(_mapper.Map>(movieEntities)); 31 | } 32 | 33 | 34 | [HttpGet("{movieId}", Name = "GetMovie")] 35 | public async Task> GetMovie(Guid movieId) 36 | { 37 | var movieEntity = await _moviesRepository.GetMovieAsync(movieId); 38 | if (movieEntity == null) 39 | { 40 | return NotFound(); 41 | } 42 | 43 | return Ok(_mapper.Map(movieEntity)); 44 | } 45 | 46 | [HttpPost] 47 | public async Task CreateMovie( 48 | [FromBody] Models.MovieForCreation movieForCreation) 49 | { 50 | var movieEntity = _mapper.Map(movieForCreation); 51 | _moviesRepository.AddMovie(movieEntity); 52 | 53 | // save the changes 54 | await _moviesRepository.SaveChangesAsync(); 55 | 56 | // Fetch the movie from the data store so the director is included 57 | await _moviesRepository.GetMovieAsync(movieEntity.Id); 58 | 59 | return CreatedAtRoute("GetMovie", 60 | new { movieId = movieEntity.Id }, 61 | _mapper.Map(movieEntity)); 62 | } 63 | 64 | [HttpPut("{movieId}")] 65 | public async Task UpdateMovie(Guid movieId, 66 | [FromBody] Models.MovieForUpdate movieForUpdate) 67 | { 68 | var movieEntity = await _moviesRepository.GetMovieAsync(movieId); 69 | if (movieEntity == null) 70 | { 71 | return NotFound(); 72 | } 73 | 74 | // map the inputted object into the movie entity 75 | // this ensures properties will get updated 76 | _mapper.Map(movieForUpdate, movieEntity); 77 | 78 | // call into UpdateMovie even though in our implementation 79 | // this doesn't contain code - doing this ensures the code stays 80 | // reliable when other repository implemenations (eg: a mock 81 | // repository) are used. 82 | _moviesRepository.UpdateMovie(movieEntity); 83 | 84 | await _moviesRepository.SaveChangesAsync(); 85 | 86 | // return the updated movie, after mapping it 87 | return Ok(_mapper.Map(movieEntity)); 88 | } 89 | 90 | [HttpPatch("{movieId}")] 91 | public async Task PartiallyUpdateMovie(Guid movieId, 92 | [FromBody] JsonPatchDocument patchDoc) 93 | { 94 | var movieEntity = await _moviesRepository.GetMovieAsync(movieId); 95 | if (movieEntity == null) 96 | { 97 | return NotFound(); 98 | } 99 | 100 | // the patch is on a DTO, not on the movie entity 101 | var movieToPatch = _mapper.Map(movieEntity); 102 | 103 | patchDoc.ApplyTo(movieToPatch, ModelState); 104 | 105 | if (!ModelState.IsValid) 106 | { 107 | return new UnprocessableEntityObjectResult(ModelState); 108 | } 109 | 110 | // map back to the entity, and save 111 | _mapper.Map(movieToPatch, movieEntity); 112 | 113 | // call into UpdateMovie even though in our implementation 114 | // this doesn't contain code - doing this ensures the code stays 115 | // reliable when other repository implemenations (eg: a mock 116 | // repository) are used. 117 | _moviesRepository.UpdateMovie(movieEntity); 118 | 119 | await _moviesRepository.SaveChangesAsync(); 120 | 121 | // return the updated movie, after mapping it 122 | return Ok(_mapper.Map(movieEntity)); 123 | } 124 | 125 | [HttpDelete("{movieid}")] 126 | public async Task DeleteMovie(Guid movieId) 127 | { 128 | var movieEntity = await _moviesRepository.GetMovieAsync(movieId); 129 | if (movieEntity == null) 130 | { 131 | return NotFound(); 132 | } 133 | 134 | _moviesRepository.DeleteMovie(movieEntity); 135 | await _moviesRepository.SaveChangesAsync(); 136 | 137 | return NoContent(); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Finished sample/Movies.Client/Services/FaultsAndErrorsSamples.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Movies.Client.Helpers; 3 | using Movies.Client.Models; 4 | using System.Net; 5 | using System.Net.Http.Headers; 6 | using System.Text.Json; 7 | 8 | namespace Movies.Client.Services; 9 | 10 | public class FaultsAndErrorsSamples : IIntegrationService 11 | { 12 | private readonly IHttpClientFactory _httpClientFactory; 13 | private readonly JsonSerializerOptionsWrapper _jsonSerializerOptionsWrapper; 14 | 15 | public FaultsAndErrorsSamples(IHttpClientFactory httpClientFactory, 16 | JsonSerializerOptionsWrapper jsonSerializerOptionsWrapper) 17 | { 18 | _jsonSerializerOptionsWrapper = jsonSerializerOptionsWrapper ?? 19 | throw new ArgumentNullException(nameof(jsonSerializerOptionsWrapper)); 20 | _httpClientFactory = httpClientFactory ?? 21 | throw new ArgumentNullException(nameof(httpClientFactory)); 22 | } 23 | 24 | public async Task RunAsync() 25 | { 26 | // await GetMovieAndDealWithInvalidResponsesAsync(CancellationToken.None); 27 | await PostMovieAndHandleErrorsAsync(CancellationToken.None); 28 | } 29 | 30 | private async Task GetMovieAndDealWithInvalidResponsesAsync( 31 | CancellationToken cancellationToken) 32 | { 33 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 34 | 35 | var request = new HttpRequestMessage( 36 | HttpMethod.Get, 37 | "api/movies/030a43b0-f9a5-405a-811c-bf342524b2be"); 38 | request.Headers.Accept.Add( 39 | new MediaTypeWithQualityHeaderValue("application/json")); 40 | request.Headers.AcceptEncoding.Add( 41 | new StringWithQualityHeaderValue("gzip")); 42 | 43 | using (var response = await httpClient.SendAsync(request, 44 | HttpCompletionOption.ResponseHeadersRead, 45 | cancellationToken)) 46 | { 47 | if (!response.IsSuccessStatusCode) 48 | { 49 | // inspect the status code 50 | if (response.StatusCode == HttpStatusCode.NotFound) 51 | { 52 | // show this to the user 53 | Console.WriteLine("The requested movie cannot be found."); 54 | return; 55 | } 56 | else if (response.StatusCode == HttpStatusCode.Unauthorized) 57 | { 58 | // trigger a login flow 59 | return; 60 | } 61 | response.EnsureSuccessStatusCode(); 62 | } 63 | 64 | var stream = await response.Content.ReadAsStreamAsync(); 65 | var movie = await JsonSerializer.DeserializeAsync( 66 | stream, 67 | _jsonSerializerOptionsWrapper.Options); 68 | 69 | } 70 | } 71 | 72 | private async Task PostMovieAndHandleErrorsAsync( 73 | CancellationToken cancellationToken) 74 | { 75 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 76 | 77 | var movieForCreation = new MovieForCreation(); 78 | 79 | var serializedMovieForCreation = JsonSerializer.Serialize( 80 | movieForCreation, 81 | _jsonSerializerOptionsWrapper.Options); 82 | 83 | using (var request = new HttpRequestMessage( 84 | HttpMethod.Post, 85 | "api/movies")) 86 | { 87 | request.Headers.Accept.Add( 88 | new MediaTypeWithQualityHeaderValue("application/json")); 89 | request.Headers.AcceptEncoding.Add( 90 | new StringWithQualityHeaderValue("gzip")); 91 | request.Content = new StringContent(serializedMovieForCreation); 92 | request.Content.Headers.ContentType = 93 | new MediaTypeHeaderValue("application/json"); 94 | 95 | using (var response = await httpClient.SendAsync(request, 96 | HttpCompletionOption.ResponseHeadersRead, 97 | cancellationToken)) 98 | { 99 | if (!response.IsSuccessStatusCode) 100 | { 101 | // inspect the status code 102 | if (response.StatusCode == HttpStatusCode.BadRequest) 103 | { 104 | // read out the response body and log it to the console window 105 | var errorStream = await response.Content.ReadAsStreamAsync(); 106 | 107 | var errorAsProblemDetails = await JsonSerializer.DeserializeAsync( 108 | errorStream, 109 | _jsonSerializerOptionsWrapper.Options); 110 | 111 | var errors = errorAsProblemDetails?.Errors; 112 | Console.WriteLine(errorAsProblemDetails?.Title); 113 | 114 | return; 115 | } 116 | else if (response.StatusCode == HttpStatusCode.Unauthorized) 117 | { 118 | // trigger a login flow 119 | return; 120 | } 121 | response.EnsureSuccessStatusCode(); 122 | } 123 | 124 | var stream = await response.Content.ReadAsStreamAsync(); 125 | var movie = await JsonSerializer.DeserializeAsync( 126 | stream, 127 | _jsonSerializerOptionsWrapper.Options); 128 | } 129 | } 130 | } 131 | 132 | 133 | } -------------------------------------------------------------------------------- /Starter files/Movies.API/Migrations/InitialMigration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | #pragma warning disable CA1814 // Prefer jagged arrays over multidimensional 7 | 8 | namespace Movies.API.Migrations 9 | { 10 | /// 11 | public partial class InitialMigration : Migration 12 | { 13 | /// 14 | protected override void Up(MigrationBuilder migrationBuilder) 15 | { 16 | migrationBuilder.CreateTable( 17 | name: "Directors", 18 | columns: table => new 19 | { 20 | Id = table.Column(type: "TEXT", nullable: false), 21 | FirstName = table.Column(type: "TEXT", maxLength: 200, nullable: false), 22 | LastName = table.Column(type: "TEXT", maxLength: 200, nullable: false) 23 | }, 24 | constraints: table => 25 | { 26 | table.PrimaryKey("PK_Directors", x => x.Id); 27 | }); 28 | 29 | migrationBuilder.CreateTable( 30 | name: "Movies", 31 | columns: table => new 32 | { 33 | Id = table.Column(type: "TEXT", nullable: false), 34 | Title = table.Column(type: "TEXT", maxLength: 200, nullable: false), 35 | Description = table.Column(type: "TEXT", maxLength: 2000, nullable: true), 36 | Genre = table.Column(type: "TEXT", maxLength: 200, nullable: true), 37 | ReleaseDate = table.Column(type: "TEXT", nullable: false), 38 | DirectorId = table.Column(type: "TEXT", nullable: false) 39 | }, 40 | constraints: table => 41 | { 42 | table.PrimaryKey("PK_Movies", x => x.Id); 43 | table.ForeignKey( 44 | name: "FK_Movies_Directors_DirectorId", 45 | column: x => x.DirectorId, 46 | principalTable: "Directors", 47 | principalColumn: "Id", 48 | onDelete: ReferentialAction.Cascade); 49 | }); 50 | 51 | migrationBuilder.InsertData( 52 | table: "Directors", 53 | columns: new[] { "Id", "FirstName", "LastName" }, 54 | values: new object[,] 55 | { 56 | { new Guid("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), "David", "Fincher" }, 57 | { new Guid("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), "James", "Cameron" }, 58 | { new Guid("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), "Bryan", "Singer" }, 59 | { new Guid("c19099ed-94db-44ba-885b-0ad7205d5e40"), "Martin", "Scorsese" }, 60 | { new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), "Quentin", "Tarantino" }, 61 | { new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), "Joel", "Coen" } 62 | }); 63 | 64 | migrationBuilder.InsertData( 65 | table: "Movies", 66 | columns: new[] { "Id", "Description", "DirectorId", "Genre", "ReleaseDate", "Title" }, 67 | values: new object[,] 68 | { 69 | { new Guid("26fcbcc4-b7f7-47fc-9382-740c12246b59"), "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.", 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 | { 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" }, 71 | { 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" }, 72 | { 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" }, 73 | { 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" }, 74 | { 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" }, 75 | { 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" } 76 | }); 77 | 78 | migrationBuilder.CreateIndex( 79 | name: "IX_Movies_DirectorId", 80 | table: "Movies", 81 | column: "DirectorId"); 82 | } 83 | 84 | /// 85 | protected override void Down(MigrationBuilder migrationBuilder) 86 | { 87 | migrationBuilder.DropTable( 88 | name: "Movies"); 89 | 90 | migrationBuilder.DropTable( 91 | name: "Directors"); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Migrations/InitialMigration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | #pragma warning disable CA1814 // Prefer jagged arrays over multidimensional 7 | 8 | namespace Movies.API.Migrations 9 | { 10 | /// 11 | public partial class InitialMigration : Migration 12 | { 13 | /// 14 | protected override void Up(MigrationBuilder migrationBuilder) 15 | { 16 | migrationBuilder.CreateTable( 17 | name: "Directors", 18 | columns: table => new 19 | { 20 | Id = table.Column(type: "TEXT", nullable: false), 21 | FirstName = table.Column(type: "TEXT", maxLength: 200, nullable: false), 22 | LastName = table.Column(type: "TEXT", maxLength: 200, nullable: false) 23 | }, 24 | constraints: table => 25 | { 26 | table.PrimaryKey("PK_Directors", x => x.Id); 27 | }); 28 | 29 | migrationBuilder.CreateTable( 30 | name: "Movies", 31 | columns: table => new 32 | { 33 | Id = table.Column(type: "TEXT", nullable: false), 34 | Title = table.Column(type: "TEXT", maxLength: 200, nullable: false), 35 | Description = table.Column(type: "TEXT", maxLength: 2000, nullable: true), 36 | Genre = table.Column(type: "TEXT", maxLength: 200, nullable: true), 37 | ReleaseDate = table.Column(type: "TEXT", nullable: false), 38 | DirectorId = table.Column(type: "TEXT", nullable: false) 39 | }, 40 | constraints: table => 41 | { 42 | table.PrimaryKey("PK_Movies", x => x.Id); 43 | table.ForeignKey( 44 | name: "FK_Movies_Directors_DirectorId", 45 | column: x => x.DirectorId, 46 | principalTable: "Directors", 47 | principalColumn: "Id", 48 | onDelete: ReferentialAction.Cascade); 49 | }); 50 | 51 | migrationBuilder.InsertData( 52 | table: "Directors", 53 | columns: new[] { "Id", "FirstName", "LastName" }, 54 | values: new object[,] 55 | { 56 | { new Guid("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), "David", "Fincher" }, 57 | { new Guid("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), "James", "Cameron" }, 58 | { new Guid("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), "Bryan", "Singer" }, 59 | { new Guid("c19099ed-94db-44ba-885b-0ad7205d5e40"), "Martin", "Scorsese" }, 60 | { new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), "Quentin", "Tarantino" }, 61 | { new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), "Joel", "Coen" } 62 | }); 63 | 64 | migrationBuilder.InsertData( 65 | table: "Movies", 66 | columns: new[] { "Id", "Description", "DirectorId", "Genre", "ReleaseDate", "Title" }, 67 | values: new object[,] 68 | { 69 | { new Guid("26fcbcc4-b7f7-47fc-9382-740c12246b59"), "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.", 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 | { 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" }, 71 | { 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" }, 72 | { 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" }, 73 | { 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" }, 74 | { 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" }, 75 | { 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" } 76 | }); 77 | 78 | migrationBuilder.CreateIndex( 79 | name: "IX_Movies_DirectorId", 80 | table: "Movies", 81 | column: "DirectorId"); 82 | } 83 | 84 | /// 85 | protected override void Down(MigrationBuilder migrationBuilder) 86 | { 87 | migrationBuilder.DropTable( 88 | name: "Movies"); 89 | 90 | migrationBuilder.DropTable( 91 | name: "Directors"); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | 400 | # SQLite temporary files 401 | *.db-shm 402 | *.db-wal -------------------------------------------------------------------------------- /Finished sample/Movies.Client/Services/CRUDSamples.cs: -------------------------------------------------------------------------------- 1 | using Movies.Client.Helpers; 2 | using Movies.Client.Models; 3 | using System.Net.Http; 4 | using System.Net.Http.Headers; 5 | using System.Text; 6 | using System.Text.Json; 7 | using System.Xml.Serialization; 8 | 9 | namespace Movies.Client.Services; 10 | 11 | public class CRUDSamples : IIntegrationService 12 | { 13 | private readonly IHttpClientFactory _httpClientFactory; 14 | private readonly JsonSerializerOptionsWrapper _jsonSerializerOptionsWrapper; 15 | 16 | public CRUDSamples(IHttpClientFactory httpClientFactory, 17 | JsonSerializerOptionsWrapper jsonSerializerOptionsWrapper) 18 | { 19 | _jsonSerializerOptionsWrapper = jsonSerializerOptionsWrapper ?? 20 | throw new ArgumentNullException(nameof(jsonSerializerOptionsWrapper)); 21 | _httpClientFactory = httpClientFactory ?? 22 | throw new ArgumentNullException(nameof(httpClientFactory)); 23 | } 24 | 25 | 26 | public async Task RunAsync() 27 | { 28 | // await GetResourceAsync(); 29 | // await GetResourceThroughHttpRequestMessageAsync(); 30 | // await CreateResourceAsync(); 31 | // await UpdateResourceAsync(); 32 | await DeleteResourceAsync(); 33 | } 34 | 35 | public async Task GetResourceAsync() 36 | { 37 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 38 | 39 | httpClient.DefaultRequestHeaders.Clear(); 40 | httpClient.DefaultRequestHeaders.Accept.Add( 41 | new MediaTypeWithQualityHeaderValue("application/json")); 42 | httpClient.DefaultRequestHeaders.Accept.Add( 43 | new MediaTypeWithQualityHeaderValue( 44 | "application/xml", 45 | 0.9)); 46 | 47 | var response = await httpClient.GetAsync("api/movies"); 48 | response.EnsureSuccessStatusCode(); 49 | 50 | var content = await response.Content.ReadAsStringAsync(); 51 | 52 | var movies = new List(); 53 | 54 | if (response.Content.Headers.ContentType?.MediaType 55 | == "application/json") 56 | { 57 | movies = JsonSerializer.Deserialize>( 58 | content, 59 | _jsonSerializerOptionsWrapper.Options); 60 | } 61 | else if (response.Content.Headers.ContentType?.MediaType 62 | == "application/xml") 63 | { 64 | var serializer = new XmlSerializer(typeof(List)); 65 | movies = serializer.Deserialize( 66 | new StringReader(content)) as List; 67 | } 68 | } 69 | 70 | public async Task GetResourceThroughHttpRequestMessageAsync() 71 | { 72 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 73 | 74 | var request = new HttpRequestMessage( 75 | HttpMethod.Get, 76 | "api/movies"); 77 | request.Headers.Accept.Add( 78 | new MediaTypeWithQualityHeaderValue("application/json")); 79 | 80 | var response = await httpClient.SendAsync(request); 81 | response.EnsureSuccessStatusCode(); 82 | 83 | var content = await response.Content.ReadAsStringAsync(); 84 | 85 | var movies = JsonSerializer.Deserialize>( 86 | content, 87 | _jsonSerializerOptionsWrapper.Options); 88 | } 89 | 90 | public async Task CreateResourceAsync() 91 | { 92 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 93 | var movieToCreate = new MovieForCreation() 94 | { 95 | Title = "Reservoir Dogs", 96 | Description = "After a simple jewelry heist goes terribly wrong, the " + 97 | "surviving criminals begin to suspect that one of them is a police informant.", 98 | DirectorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 99 | ReleaseDate = new DateTimeOffset(new DateTime(1992, 9, 2)), 100 | Genre = "Crime, Drama" 101 | }; 102 | 103 | var serializedMovieToCreate = JsonSerializer.Serialize( 104 | movieToCreate, 105 | _jsonSerializerOptionsWrapper.Options); 106 | 107 | var request = new HttpRequestMessage( 108 | HttpMethod.Post, 109 | "api/movies"); 110 | request.Headers.Accept.Add( 111 | new MediaTypeWithQualityHeaderValue("application/json")); 112 | 113 | request.Content = new StringContent(serializedMovieToCreate); 114 | request.Content.Headers.ContentType = 115 | new MediaTypeHeaderValue("application/json"); 116 | 117 | var response = await httpClient.SendAsync(request); 118 | response.EnsureSuccessStatusCode(); 119 | 120 | var content = await response.Content.ReadAsStringAsync(); 121 | 122 | var createdMovie = JsonSerializer.Deserialize( 123 | content, 124 | _jsonSerializerOptionsWrapper.Options); 125 | 126 | } 127 | 128 | public async Task UpdateResourceAsync() 129 | { 130 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 131 | 132 | var movieToUpdate = new MovieForUpdate() 133 | { 134 | Title = "Pulp Fiction", 135 | Description = "The movie with Zed.", 136 | DirectorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 137 | ReleaseDate = new DateTimeOffset(new DateTime(1992, 9, 2)), 138 | Genre = "Crime, Drama" 139 | }; 140 | 141 | var serializedMovieToUpdate = JsonSerializer.Serialize( 142 | movieToUpdate, 143 | _jsonSerializerOptionsWrapper.Options); 144 | 145 | var request = new HttpRequestMessage( 146 | HttpMethod.Put, 147 | "api/movies/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"); 148 | request.Headers.Accept.Add( 149 | new MediaTypeWithQualityHeaderValue("application/json")); 150 | request.Content = new StringContent(serializedMovieToUpdate); 151 | request.Content.Headers.ContentType = 152 | new MediaTypeHeaderValue("application/json"); 153 | 154 | var response = await httpClient.SendAsync(request); 155 | response.EnsureSuccessStatusCode(); 156 | 157 | var content = await response.Content.ReadAsStringAsync(); 158 | var updatedMovie = JsonSerializer.Deserialize( 159 | content, 160 | _jsonSerializerOptionsWrapper.Options); 161 | } 162 | 163 | public async Task DeleteResourceAsync() 164 | { 165 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 166 | 167 | var request = new HttpRequestMessage( 168 | HttpMethod.Delete, 169 | "api/movies/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"); 170 | request.Headers.Accept.Add( 171 | new MediaTypeWithQualityHeaderValue("application/json")); 172 | 173 | var response = await httpClient.SendAsync(request); 174 | response.EnsureSuccessStatusCode(); 175 | 176 | var content = await response.Content.ReadAsStringAsync(); 177 | } 178 | 179 | private async Task PostResourceShortcutAsync() 180 | { 181 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 182 | 183 | var movieToCreate = new MovieForCreation() 184 | { 185 | Title = "Reservoir Dogs", 186 | Description = "After a simple jewelry heist goes terribly wrong, the " + 187 | "surviving criminals begin to suspect that one of them is a police informant.", 188 | DirectorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 189 | ReleaseDate = new DateTimeOffset(new DateTime(1992, 9, 2)), 190 | Genre = "Crime, Drama" 191 | }; 192 | 193 | var response = await httpClient.PostAsync( 194 | "api/movies", 195 | new StringContent( 196 | JsonSerializer.Serialize(movieToCreate, 197 | _jsonSerializerOptionsWrapper.Options), 198 | Encoding.UTF8, 199 | "application/json")); 200 | 201 | response.EnsureSuccessStatusCode(); 202 | 203 | var content = await response.Content.ReadAsStringAsync(); 204 | var createdMovie = JsonSerializer.Deserialize( 205 | content, 206 | _jsonSerializerOptionsWrapper.Options); 207 | } 208 | 209 | private async Task PutResourceShortcut() 210 | { 211 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 212 | 213 | var movieToUpdate = new MovieForUpdate() 214 | { 215 | Title = "Pulp Fiction", 216 | Description = "The movie with Zed.", 217 | DirectorId = Guid.Parse("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 218 | ReleaseDate = new DateTimeOffset(new DateTime(1992, 9, 2)), 219 | Genre = "Crime, Drama" 220 | }; 221 | 222 | var response = await httpClient.PutAsync( 223 | "api/movies/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b", 224 | new StringContent( 225 | JsonSerializer.Serialize(movieToUpdate, 226 | _jsonSerializerOptionsWrapper.Options), 227 | System.Text.Encoding.UTF8, 228 | "application/json")); 229 | 230 | response.EnsureSuccessStatusCode(); 231 | 232 | var content = await response.Content.ReadAsStringAsync(); 233 | var updatedMovie = JsonSerializer.Deserialize( 234 | content, 235 | _jsonSerializerOptionsWrapper.Options); 236 | } 237 | 238 | private async Task DeleteResourceShortcut() 239 | { 240 | var httpClient = _httpClientFactory.CreateClient("MoviesAPIClient"); 241 | 242 | var response = await httpClient.DeleteAsync( 243 | "api/movies/5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"); 244 | response.EnsureSuccessStatusCode(); 245 | 246 | var content = await response.Content.ReadAsStringAsync(); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Migrations/MoviesDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using Movies.API.DbContexts; 7 | 8 | #nullable disable 9 | 10 | namespace Movies.API.Migrations 11 | { 12 | [DbContext(typeof(MoviesDbContext))] 13 | partial class MoviesDbContextModelSnapshot : ModelSnapshot 14 | { 15 | protected override void BuildModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); 19 | 20 | modelBuilder.Entity("Movies.API.Entities.Director", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd() 24 | .HasColumnType("TEXT"); 25 | 26 | b.Property("FirstName") 27 | .IsRequired() 28 | .HasMaxLength(200) 29 | .HasColumnType("TEXT"); 30 | 31 | b.Property("LastName") 32 | .IsRequired() 33 | .HasMaxLength(200) 34 | .HasColumnType("TEXT"); 35 | 36 | b.HasKey("Id"); 37 | 38 | b.ToTable("Directors"); 39 | 40 | b.HasData( 41 | new 42 | { 43 | Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 44 | FirstName = "Quentin", 45 | LastName = "Tarantino" 46 | }, 47 | new 48 | { 49 | Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 50 | FirstName = "Joel", 51 | LastName = "Coen" 52 | }, 53 | new 54 | { 55 | Id = new Guid("c19099ed-94db-44ba-885b-0ad7205d5e40"), 56 | FirstName = "Martin", 57 | LastName = "Scorsese" 58 | }, 59 | new 60 | { 61 | Id = new Guid("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), 62 | FirstName = "David", 63 | LastName = "Fincher" 64 | }, 65 | new 66 | { 67 | Id = new Guid("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), 68 | FirstName = "Bryan", 69 | LastName = "Singer" 70 | }, 71 | new 72 | { 73 | Id = new Guid("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), 74 | FirstName = "James", 75 | LastName = "Cameron" 76 | }); 77 | }); 78 | 79 | modelBuilder.Entity("Movies.API.Entities.Movie", b => 80 | { 81 | b.Property("Id") 82 | .ValueGeneratedOnAdd() 83 | .HasColumnType("TEXT"); 84 | 85 | b.Property("Description") 86 | .HasMaxLength(2000) 87 | .HasColumnType("TEXT"); 88 | 89 | b.Property("DirectorId") 90 | .HasColumnType("TEXT"); 91 | 92 | b.Property("Genre") 93 | .HasMaxLength(200) 94 | .HasColumnType("TEXT"); 95 | 96 | b.Property("ReleaseDate") 97 | .HasColumnType("TEXT"); 98 | 99 | b.Property("Title") 100 | .IsRequired() 101 | .HasMaxLength(200) 102 | .HasColumnType("TEXT"); 103 | 104 | b.HasKey("Id"); 105 | 106 | b.HasIndex("DirectorId"); 107 | 108 | b.ToTable("Movies"); 109 | 110 | b.HasData( 111 | new 112 | { 113 | Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 114 | 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.", 115 | DirectorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 116 | Genre = "Crime, Drama", 117 | ReleaseDate = new DateTimeOffset(new DateTime(1994, 11, 9, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 118 | Title = "Pulp Fiction" 119 | }, 120 | new 121 | { 122 | Id = new Guid("6e87f657-f2c1-4d90-9b37-cbe43cc6adb9"), 123 | 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.", 124 | DirectorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 125 | Genre = "Crime, Drama", 126 | ReleaseDate = new DateTimeOffset(new DateTime(1997, 12, 25, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 127 | Title = "Jackie Brown" 128 | }, 129 | new 130 | { 131 | Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 132 | Description = "The Dude (Lebowski), mistaken for a millionaire Lebowski, seeks restitution for his ruined rug and enlists his bowling buddies to help get it.", 133 | DirectorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 134 | Genre = "Comedy, Crime", 135 | ReleaseDate = new DateTimeOffset(new DateTime(1998, 3, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 136 | Title = "The Big Lebowski" 137 | }, 138 | new 139 | { 140 | Id = new Guid("f9a16fee-4c49-41bb-87a1-bbaad0cd1174"), 141 | 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.", 142 | DirectorId = new Guid("c19099ed-94db-44ba-885b-0ad7205d5e40"), 143 | Genre = "Crime, Drama", 144 | ReleaseDate = new DateTimeOffset(new DateTime(1995, 11, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 145 | Title = "Casino" 146 | }, 147 | new 148 | { 149 | Id = new Guid("bb6a100a-053f-4bf8-b271-60ce3aae6eb5"), 150 | Description = "An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.", 151 | DirectorId = new Guid("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), 152 | Genre = "Drama", 153 | ReleaseDate = new DateTimeOffset(new DateTime(1999, 10, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 154 | Title = "Fight Club" 155 | }, 156 | new 157 | { 158 | Id = new Guid("3d2880ae-5ba6-417c-845d-f4ebfd4bcac7"), 159 | 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.", 160 | DirectorId = new Guid("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), 161 | Genre = "Crime, Thriller", 162 | ReleaseDate = new DateTimeOffset(new DateTime(1995, 9, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 163 | Title = "The Usual Suspects" 164 | }, 165 | new 166 | { 167 | Id = new Guid("26fcbcc4-b7f7-47fc-9382-740c12246b59"), 168 | 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.", 169 | DirectorId = new Guid("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), 170 | Genre = "Action, Sci-Fi", 171 | ReleaseDate = new DateTimeOffset(new DateTime(1991, 7, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 172 | Title = "Terminator 2: Judgment Day" 173 | }); 174 | }); 175 | 176 | modelBuilder.Entity("Movies.API.Entities.Movie", b => 177 | { 178 | b.HasOne("Movies.API.Entities.Director", "Director") 179 | .WithMany() 180 | .HasForeignKey("DirectorId") 181 | .OnDelete(DeleteBehavior.Cascade) 182 | .IsRequired(); 183 | 184 | b.Navigation("Director"); 185 | }); 186 | #pragma warning restore 612, 618 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Migrations/MoviesDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using Movies.API.DbContexts; 7 | 8 | #nullable disable 9 | 10 | namespace Movies.API.Migrations 11 | { 12 | [DbContext(typeof(MoviesDbContext))] 13 | partial class MoviesDbContextModelSnapshot : ModelSnapshot 14 | { 15 | protected override void BuildModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); 19 | 20 | modelBuilder.Entity("Movies.API.Entities.Director", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd() 24 | .HasColumnType("TEXT"); 25 | 26 | b.Property("FirstName") 27 | .IsRequired() 28 | .HasMaxLength(200) 29 | .HasColumnType("TEXT"); 30 | 31 | b.Property("LastName") 32 | .IsRequired() 33 | .HasMaxLength(200) 34 | .HasColumnType("TEXT"); 35 | 36 | b.HasKey("Id"); 37 | 38 | b.ToTable("Directors"); 39 | 40 | b.HasData( 41 | new 42 | { 43 | Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 44 | FirstName = "Quentin", 45 | LastName = "Tarantino" 46 | }, 47 | new 48 | { 49 | Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 50 | FirstName = "Joel", 51 | LastName = "Coen" 52 | }, 53 | new 54 | { 55 | Id = new Guid("c19099ed-94db-44ba-885b-0ad7205d5e40"), 56 | FirstName = "Martin", 57 | LastName = "Scorsese" 58 | }, 59 | new 60 | { 61 | Id = new Guid("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), 62 | FirstName = "David", 63 | LastName = "Fincher" 64 | }, 65 | new 66 | { 67 | Id = new Guid("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), 68 | FirstName = "Bryan", 69 | LastName = "Singer" 70 | }, 71 | new 72 | { 73 | Id = new Guid("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), 74 | FirstName = "James", 75 | LastName = "Cameron" 76 | }); 77 | }); 78 | 79 | modelBuilder.Entity("Movies.API.Entities.Movie", b => 80 | { 81 | b.Property("Id") 82 | .ValueGeneratedOnAdd() 83 | .HasColumnType("TEXT"); 84 | 85 | b.Property("Description") 86 | .HasMaxLength(2000) 87 | .HasColumnType("TEXT"); 88 | 89 | b.Property("DirectorId") 90 | .HasColumnType("TEXT"); 91 | 92 | b.Property("Genre") 93 | .HasMaxLength(200) 94 | .HasColumnType("TEXT"); 95 | 96 | b.Property("ReleaseDate") 97 | .HasColumnType("TEXT"); 98 | 99 | b.Property("Title") 100 | .IsRequired() 101 | .HasMaxLength(200) 102 | .HasColumnType("TEXT"); 103 | 104 | b.HasKey("Id"); 105 | 106 | b.HasIndex("DirectorId"); 107 | 108 | b.ToTable("Movies"); 109 | 110 | b.HasData( 111 | new 112 | { 113 | Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 114 | 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.", 115 | DirectorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 116 | Genre = "Crime, Drama", 117 | ReleaseDate = new DateTimeOffset(new DateTime(1994, 11, 9, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 118 | Title = "Pulp Fiction" 119 | }, 120 | new 121 | { 122 | Id = new Guid("6e87f657-f2c1-4d90-9b37-cbe43cc6adb9"), 123 | 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.", 124 | DirectorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 125 | Genre = "Crime, Drama", 126 | ReleaseDate = new DateTimeOffset(new DateTime(1997, 12, 25, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 127 | Title = "Jackie Brown" 128 | }, 129 | new 130 | { 131 | Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 132 | Description = "The Dude (Lebowski), mistaken for a millionaire Lebowski, seeks restitution for his ruined rug and enlists his bowling buddies to help get it.", 133 | DirectorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 134 | Genre = "Comedy, Crime", 135 | ReleaseDate = new DateTimeOffset(new DateTime(1998, 3, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 136 | Title = "The Big Lebowski" 137 | }, 138 | new 139 | { 140 | Id = new Guid("f9a16fee-4c49-41bb-87a1-bbaad0cd1174"), 141 | 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.", 142 | DirectorId = new Guid("c19099ed-94db-44ba-885b-0ad7205d5e40"), 143 | Genre = "Crime, Drama", 144 | ReleaseDate = new DateTimeOffset(new DateTime(1995, 11, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 145 | Title = "Casino" 146 | }, 147 | new 148 | { 149 | Id = new Guid("bb6a100a-053f-4bf8-b271-60ce3aae6eb5"), 150 | Description = "An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.", 151 | DirectorId = new Guid("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), 152 | Genre = "Drama", 153 | ReleaseDate = new DateTimeOffset(new DateTime(1999, 10, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 154 | Title = "Fight Club" 155 | }, 156 | new 157 | { 158 | Id = new Guid("3d2880ae-5ba6-417c-845d-f4ebfd4bcac7"), 159 | 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.", 160 | DirectorId = new Guid("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), 161 | Genre = "Crime, Thriller", 162 | ReleaseDate = new DateTimeOffset(new DateTime(1995, 9, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 163 | Title = "The Usual Suspects" 164 | }, 165 | new 166 | { 167 | Id = new Guid("26fcbcc4-b7f7-47fc-9382-740c12246b59"), 168 | 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.", 169 | DirectorId = new Guid("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), 170 | Genre = "Action, Sci-Fi", 171 | ReleaseDate = new DateTimeOffset(new DateTime(1991, 7, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 172 | Title = "Terminator 2: Judgment Day" 173 | }); 174 | }); 175 | 176 | modelBuilder.Entity("Movies.API.Entities.Movie", b => 177 | { 178 | b.HasOne("Movies.API.Entities.Director", "Director") 179 | .WithMany() 180 | .HasForeignKey("DirectorId") 181 | .OnDelete(DeleteBehavior.Cascade) 182 | .IsRequired(); 183 | 184 | b.Navigation("Director"); 185 | }); 186 | #pragma warning restore 612, 618 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Finished sample/Movies.API/Migrations/InitialMigration.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Movies.API.DbContexts; 8 | 9 | #nullable disable 10 | 11 | namespace Movies.API.Migrations 12 | { 13 | [DbContext(typeof(MoviesDbContext))] 14 | [Migration("20240129132613_InitialMigration")] 15 | partial class InitialMigration 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); 22 | 23 | modelBuilder.Entity("Movies.API.Entities.Director", b => 24 | { 25 | b.Property("Id") 26 | .ValueGeneratedOnAdd() 27 | .HasColumnType("TEXT"); 28 | 29 | b.Property("FirstName") 30 | .IsRequired() 31 | .HasMaxLength(200) 32 | .HasColumnType("TEXT"); 33 | 34 | b.Property("LastName") 35 | .IsRequired() 36 | .HasMaxLength(200) 37 | .HasColumnType("TEXT"); 38 | 39 | b.HasKey("Id"); 40 | 41 | b.ToTable("Directors"); 42 | 43 | b.HasData( 44 | new 45 | { 46 | Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 47 | FirstName = "Quentin", 48 | LastName = "Tarantino" 49 | }, 50 | new 51 | { 52 | Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 53 | FirstName = "Joel", 54 | LastName = "Coen" 55 | }, 56 | new 57 | { 58 | Id = new Guid("c19099ed-94db-44ba-885b-0ad7205d5e40"), 59 | FirstName = "Martin", 60 | LastName = "Scorsese" 61 | }, 62 | new 63 | { 64 | Id = new Guid("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), 65 | FirstName = "David", 66 | LastName = "Fincher" 67 | }, 68 | new 69 | { 70 | Id = new Guid("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), 71 | FirstName = "Bryan", 72 | LastName = "Singer" 73 | }, 74 | new 75 | { 76 | Id = new Guid("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), 77 | FirstName = "James", 78 | LastName = "Cameron" 79 | }); 80 | }); 81 | 82 | modelBuilder.Entity("Movies.API.Entities.Movie", b => 83 | { 84 | b.Property("Id") 85 | .ValueGeneratedOnAdd() 86 | .HasColumnType("TEXT"); 87 | 88 | b.Property("Description") 89 | .HasMaxLength(2000) 90 | .HasColumnType("TEXT"); 91 | 92 | b.Property("DirectorId") 93 | .HasColumnType("TEXT"); 94 | 95 | b.Property("Genre") 96 | .HasMaxLength(200) 97 | .HasColumnType("TEXT"); 98 | 99 | b.Property("ReleaseDate") 100 | .HasColumnType("TEXT"); 101 | 102 | b.Property("Title") 103 | .IsRequired() 104 | .HasMaxLength(200) 105 | .HasColumnType("TEXT"); 106 | 107 | b.HasKey("Id"); 108 | 109 | b.HasIndex("DirectorId"); 110 | 111 | b.ToTable("Movies"); 112 | 113 | b.HasData( 114 | new 115 | { 116 | Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 117 | 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.", 118 | DirectorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 119 | Genre = "Crime, Drama", 120 | ReleaseDate = new DateTimeOffset(new DateTime(1994, 11, 9, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 121 | Title = "Pulp Fiction" 122 | }, 123 | new 124 | { 125 | Id = new Guid("6e87f657-f2c1-4d90-9b37-cbe43cc6adb9"), 126 | 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.", 127 | DirectorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 128 | Genre = "Crime, Drama", 129 | ReleaseDate = new DateTimeOffset(new DateTime(1997, 12, 25, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 130 | Title = "Jackie Brown" 131 | }, 132 | new 133 | { 134 | Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 135 | Description = "The Dude (Lebowski), mistaken for a millionaire Lebowski, seeks restitution for his ruined rug and enlists his bowling buddies to help get it.", 136 | DirectorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 137 | Genre = "Comedy, Crime", 138 | ReleaseDate = new DateTimeOffset(new DateTime(1998, 3, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 139 | Title = "The Big Lebowski" 140 | }, 141 | new 142 | { 143 | Id = new Guid("f9a16fee-4c49-41bb-87a1-bbaad0cd1174"), 144 | 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.", 145 | DirectorId = new Guid("c19099ed-94db-44ba-885b-0ad7205d5e40"), 146 | Genre = "Crime, Drama", 147 | ReleaseDate = new DateTimeOffset(new DateTime(1995, 11, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 148 | Title = "Casino" 149 | }, 150 | new 151 | { 152 | Id = new Guid("bb6a100a-053f-4bf8-b271-60ce3aae6eb5"), 153 | Description = "An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.", 154 | DirectorId = new Guid("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), 155 | Genre = "Drama", 156 | ReleaseDate = new DateTimeOffset(new DateTime(1999, 10, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 157 | Title = "Fight Club" 158 | }, 159 | new 160 | { 161 | Id = new Guid("3d2880ae-5ba6-417c-845d-f4ebfd4bcac7"), 162 | 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.", 163 | DirectorId = new Guid("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), 164 | Genre = "Crime, Thriller", 165 | ReleaseDate = new DateTimeOffset(new DateTime(1995, 9, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 166 | Title = "The Usual Suspects" 167 | }, 168 | new 169 | { 170 | Id = new Guid("26fcbcc4-b7f7-47fc-9382-740c12246b59"), 171 | 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.", 172 | DirectorId = new Guid("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), 173 | Genre = "Action, Sci-Fi", 174 | ReleaseDate = new DateTimeOffset(new DateTime(1991, 7, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 175 | Title = "Terminator 2: Judgment Day" 176 | }); 177 | }); 178 | 179 | modelBuilder.Entity("Movies.API.Entities.Movie", b => 180 | { 181 | b.HasOne("Movies.API.Entities.Director", "Director") 182 | .WithMany() 183 | .HasForeignKey("DirectorId") 184 | .OnDelete(DeleteBehavior.Cascade) 185 | .IsRequired(); 186 | 187 | b.Navigation("Director"); 188 | }); 189 | #pragma warning restore 612, 618 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Starter files/Movies.API/Migrations/InitialMigration.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Movies.API.DbContexts; 8 | 9 | #nullable disable 10 | 11 | namespace Movies.API.Migrations 12 | { 13 | [DbContext(typeof(MoviesDbContext))] 14 | [Migration("20240129132613_InitialMigration")] 15 | partial class InitialMigration 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); 22 | 23 | modelBuilder.Entity("Movies.API.Entities.Director", b => 24 | { 25 | b.Property("Id") 26 | .ValueGeneratedOnAdd() 27 | .HasColumnType("TEXT"); 28 | 29 | b.Property("FirstName") 30 | .IsRequired() 31 | .HasMaxLength(200) 32 | .HasColumnType("TEXT"); 33 | 34 | b.Property("LastName") 35 | .IsRequired() 36 | .HasMaxLength(200) 37 | .HasColumnType("TEXT"); 38 | 39 | b.HasKey("Id"); 40 | 41 | b.ToTable("Directors"); 42 | 43 | b.HasData( 44 | new 45 | { 46 | Id = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 47 | FirstName = "Quentin", 48 | LastName = "Tarantino" 49 | }, 50 | new 51 | { 52 | Id = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 53 | FirstName = "Joel", 54 | LastName = "Coen" 55 | }, 56 | new 57 | { 58 | Id = new Guid("c19099ed-94db-44ba-885b-0ad7205d5e40"), 59 | FirstName = "Martin", 60 | LastName = "Scorsese" 61 | }, 62 | new 63 | { 64 | Id = new Guid("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), 65 | FirstName = "David", 66 | LastName = "Fincher" 67 | }, 68 | new 69 | { 70 | Id = new Guid("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), 71 | FirstName = "Bryan", 72 | LastName = "Singer" 73 | }, 74 | new 75 | { 76 | Id = new Guid("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), 77 | FirstName = "James", 78 | LastName = "Cameron" 79 | }); 80 | }); 81 | 82 | modelBuilder.Entity("Movies.API.Entities.Movie", b => 83 | { 84 | b.Property("Id") 85 | .ValueGeneratedOnAdd() 86 | .HasColumnType("TEXT"); 87 | 88 | b.Property("Description") 89 | .HasMaxLength(2000) 90 | .HasColumnType("TEXT"); 91 | 92 | b.Property("DirectorId") 93 | .HasColumnType("TEXT"); 94 | 95 | b.Property("Genre") 96 | .HasMaxLength(200) 97 | .HasColumnType("TEXT"); 98 | 99 | b.Property("ReleaseDate") 100 | .HasColumnType("TEXT"); 101 | 102 | b.Property("Title") 103 | .IsRequired() 104 | .HasMaxLength(200) 105 | .HasColumnType("TEXT"); 106 | 107 | b.HasKey("Id"); 108 | 109 | b.HasIndex("DirectorId"); 110 | 111 | b.ToTable("Movies"); 112 | 113 | b.HasData( 114 | new 115 | { 116 | Id = new Guid("5b1c2b4d-48c7-402a-80c3-cc796ad49c6b"), 117 | 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.", 118 | DirectorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 119 | Genre = "Crime, Drama", 120 | ReleaseDate = new DateTimeOffset(new DateTime(1994, 11, 9, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 121 | Title = "Pulp Fiction" 122 | }, 123 | new 124 | { 125 | Id = new Guid("6e87f657-f2c1-4d90-9b37-cbe43cc6adb9"), 126 | 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.", 127 | DirectorId = new Guid("d28888e9-2ba9-473a-a40f-e38cb54f9b35"), 128 | Genre = "Crime, Drama", 129 | ReleaseDate = new DateTimeOffset(new DateTime(1997, 12, 25, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 130 | Title = "Jackie Brown" 131 | }, 132 | new 133 | { 134 | Id = new Guid("d8663e5e-7494-4f81-8739-6e0de1bea7ee"), 135 | Description = "The Dude (Lebowski), mistaken for a millionaire Lebowski, seeks restitution for his ruined rug and enlists his bowling buddies to help get it.", 136 | DirectorId = new Guid("da2fd609-d754-4feb-8acd-c4f9ff13ba96"), 137 | Genre = "Comedy, Crime", 138 | ReleaseDate = new DateTimeOffset(new DateTime(1998, 3, 6, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 139 | Title = "The Big Lebowski" 140 | }, 141 | new 142 | { 143 | Id = new Guid("f9a16fee-4c49-41bb-87a1-bbaad0cd1174"), 144 | 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.", 145 | DirectorId = new Guid("c19099ed-94db-44ba-885b-0ad7205d5e40"), 146 | Genre = "Crime, Drama", 147 | ReleaseDate = new DateTimeOffset(new DateTime(1995, 11, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 1, 0, 0, 0)), 148 | Title = "Casino" 149 | }, 150 | new 151 | { 152 | Id = new Guid("bb6a100a-053f-4bf8-b271-60ce3aae6eb5"), 153 | Description = "An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.", 154 | DirectorId = new Guid("0c4dc798-b38b-4a1c-905c-a9e76dbef17b"), 155 | Genre = "Drama", 156 | ReleaseDate = new DateTimeOffset(new DateTime(1999, 10, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 157 | Title = "Fight Club" 158 | }, 159 | new 160 | { 161 | Id = new Guid("3d2880ae-5ba6-417c-845d-f4ebfd4bcac7"), 162 | 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.", 163 | DirectorId = new Guid("937b1ba1-7969-4324-9ab5-afb0e4d875e6"), 164 | Genre = "Crime, Thriller", 165 | ReleaseDate = new DateTimeOffset(new DateTime(1995, 9, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 166 | Title = "The Usual Suspects" 167 | }, 168 | new 169 | { 170 | Id = new Guid("26fcbcc4-b7f7-47fc-9382-740c12246b59"), 171 | 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.", 172 | DirectorId = new Guid("7a2fbc72-bb33-49de-bd23-c78fceb367fc"), 173 | Genre = "Action, Sci-Fi", 174 | ReleaseDate = new DateTimeOffset(new DateTime(1991, 7, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 2, 0, 0, 0)), 175 | Title = "Terminator 2: Judgment Day" 176 | }); 177 | }); 178 | 179 | modelBuilder.Entity("Movies.API.Entities.Movie", b => 180 | { 181 | b.HasOne("Movies.API.Entities.Director", "Director") 182 | .WithMany() 183 | .HasForeignKey("DirectorId") 184 | .OnDelete(DeleteBehavior.Cascade) 185 | .IsRequired(); 186 | 187 | b.Navigation("Director"); 188 | }); 189 | #pragma warning restore 612, 618 190 | } 191 | } 192 | } 193 | --------------------------------------------------------------------------------