From 8c82b8b0de2a6d40c212a43bd3757d98788fabc4 Mon Sep 17 00:00:00 2001 From: Enock97 Date: Thu, 30 Jan 2025 08:33:07 +0100 Subject: [PATCH] + Finished core and extension exercises --- api-cinema-challenge/api-cinema-challenge.sln | 1 + .../Data/CinemaContext.cs | 10 +- .../api-cinema-challenge/Data/Seeder.cs | 58 +++++ .../Endpoints/CustomerEndpoints.cs | 65 +++++ .../Endpoints/MovieEndpoints.cs | 66 +++++ .../Endpoints/ScreeningEndpoints.cs | 36 +++ .../Endpoints/TicketEndpoints.cs | 36 +++ ...0250128091641_InitialMigration.Designer.cs | 227 ++++++++++++++++++ .../20250128091641_InitialMigration.cs | 135 +++++++++++ .../Migrations/CinemaContextModelSnapshot.cs | 224 +++++++++++++++++ .../api-cinema-challenge/Models/Customer.cs | 34 +++ .../api-cinema-challenge/Models/Movie.cs | 38 +++ .../api-cinema-challenge/Models/Screening.cs | 40 +++ .../api-cinema-challenge/Models/Ticket.cs | 34 +++ .../api-cinema-challenge/Program.cs | 22 ++ .../GenericRepositories/IRepository.cs | 11 + .../GenericRepositories/Repository.cs | 50 ++++ .../IScreeningRepository.cs | 10 + .../SpecificRepositories/ITicketRepository.cs | 12 + .../ScreeningRepository.cs | 26 ++ .../SpecificRepositories/TicketRepository.cs | 35 +++ .../api-cinema-challenge.csproj | 6 +- 22 files changed, 1170 insertions(+), 6 deletions(-) create mode 100644 api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoints.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoints.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250128091641_InitialMigration.Designer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/20250128091641_InitialMigration.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Customer.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Movie.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Screening.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repositories/GenericRepositories/IRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repositories/GenericRepositories/Repository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repositories/SpecificRepositories/IScreeningRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repositories/SpecificRepositories/ITicketRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repositories/SpecificRepositories/ScreeningRepository.cs create mode 100644 api-cinema-challenge/api-cinema-challenge/Repositories/SpecificRepositories/TicketRepository.cs diff --git a/api-cinema-challenge/api-cinema-challenge.sln b/api-cinema-challenge/api-cinema-challenge.sln index 9cd490f5..e0099dc6 100644 --- a/api-cinema-challenge/api-cinema-challenge.sln +++ b/api-cinema-challenge/api-cinema-challenge.sln @@ -8,6 +8,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3C371BAA-344D-4C8A-AF08-7829816D726F}" ProjectSection(SolutionItems) = preProject ..\.gitignore = ..\.gitignore + ..\..\..\..\Downloads\CinemaChallengeERD.drawio.png = ..\..\..\..\Downloads\CinemaChallengeERD.drawio.png ..\README.md = ..\README.md EndProjectSection EndProject diff --git a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs index ad4fe854..e8ba1390 100644 --- a/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs +++ b/api-cinema-challenge/api-cinema-challenge/Data/CinemaContext.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using api_cinema_challenge.Models; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Linq; namespace api_cinema_challenge.Data @@ -18,8 +19,15 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) optionsBuilder.UseNpgsql(_connectionString); } + // DbSet properties for each entity + public DbSet Customers { get; set; } + public DbSet Movies { get; set; } + public DbSet Screenings { get; set; } + public DbSet Tickets { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) { + base.OnModelCreating(modelBuilder); } } diff --git a/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs b/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs new file mode 100644 index 00000000..9faa7b16 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Data/Seeder.cs @@ -0,0 +1,58 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Data +{ + public static class Seeder + { + public async static Task SeedCinemaData(this WebApplication app) + { + using (var scope = app.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + + if (!db.Movies.Any()) + { + db.Movies.AddRange( + new Movie { Title = "The Matrix", Rating = "R", Description = "A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.", RuntimeMins = 136, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }, + new Movie { Title = "Inception", Rating = "PG-13", Description = "A thief who enters the dreams of others to steal secrets from their subconscious is given the inverse task of planting an idea into the mind of a CEO.", RuntimeMins = 148, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow } + ); + await db.SaveChangesAsync(); + } + + if (!db.Customers.Any()) + { + db.Customers.AddRange( + new Customer { Name = "Chris Wolstenholme", Email = "chris@muse.mu", Phone = "+44729388192", CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }, + new Customer { Name = "Jonny Greenwood", Email = "jonny@radiohead.com", Phone = "+44720488192", CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow } + ); + await db.SaveChangesAsync(); + } + + if (!db.Screenings.Any()) + { + db.Screenings.AddRange( + new Screening { MovieId = 1, ScreenNumber = 1, Capacity = 100, StartsAt = DateTime.UtcNow.AddDays(1), CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }, + new Screening { MovieId = 2, ScreenNumber = 2, Capacity = 80, StartsAt = DateTime.UtcNow.AddDays(2), CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow } + ); + await db.SaveChangesAsync(); + } + + if (!db.Tickets.Any()) + { + var customerChris = db.Customers.First(c => c.Name == "Chris Wolstenholme"); + var customerJonny = db.Customers.First(c => c.Name == "Jonny Greenwood"); + var screeningMatrix = db.Screenings.First(s => s.MovieId == 1); + var screeningInception = db.Screenings.First(s => s.MovieId == 2); + + db.Tickets.AddRange( + new Ticket { CustomerId = customerChris.Id, ScreeningId = screeningMatrix.Id, NumSeats = 2, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }, + new Ticket { CustomerId = customerJonny.Id, ScreeningId = screeningInception.Id, NumSeats = 1, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow } + ); + await db.SaveChangesAsync(); + } + } + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs new file mode 100644 index 00000000..c376321a --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/CustomerEndpoints.cs @@ -0,0 +1,65 @@ +using api_cinema_challenge.Models; +using api_cinema_challenge.Repositories.GenericRepositories; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Endpoints +{ + public static class CustomerEndpoints + { + public static void ConfigureCustomerApi(this WebApplication app) + { + var customers = app.MapGroup("/customers"); + customers.MapPost("/", CreateCustomer); + customers.MapGet("/", GetAllCustomers); + customers.MapPut("/{id}", UpdateCustomer); + customers.MapDelete("/{id}", DeleteCustomer); + + } + + // Create a Customer + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task CreateCustomer(Customer customer, IRepository repository) + { + await repository.AddAsync(customer); + return TypedResults.Created($"/customers/{customer.Id}", new { status = "success", data = customer }); + } + + // Get all Customers + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAllCustomers(IRepository repository) + { + var customers = await repository.GetAllAsync(); + return TypedResults.Ok(new { status = "success", data = customers }); + } + + // Update a Customer + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task UpdateCustomer(int id, Customer customer, IRepository repository) + { + var existingCustomer = await repository.GetByIdAsync(id); + if (existingCustomer == null) + return TypedResults.NotFound(); + + existingCustomer.Name = customer.Name; + existingCustomer.Email = customer.Email; + existingCustomer.Phone = customer.Phone; + + await repository.UpdateAsync(existingCustomer); + return TypedResults.Created($"/customers/{id}", new { status = "success", data = existingCustomer }); + } + + // Delete a Customer + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task DeleteCustomer(int id, IRepository repository) + { + var customer = await repository.GetByIdAsync(id); + if (customer == null) + return TypedResults.NotFound(); + + await repository.DeleteAsync(id); + return TypedResults.Ok(new { status = "success", data = customer }); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs new file mode 100644 index 00000000..91ec1cff --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/MovieEndpoints.cs @@ -0,0 +1,66 @@ +using api_cinema_challenge.Models; +using api_cinema_challenge.Repositories.GenericRepositories; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Endpoints +{ + public static class MovieEndpoints + { + public static void ConfigureMovieApi(this WebApplication app) + { + var movies = app.MapGroup("/movies"); + movies.MapPost("/", CreateMovie); + movies.MapGet("/", GetAllMovies); + movies.MapPut("/{id}", UpdateMovie); + movies.MapDelete("/{id}", DeleteMovie); + } + + // Create a Movie + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task CreateMovie(Movie movie, IRepository repository) + { + await repository.AddAsync(movie); + return TypedResults.Created($"/movies/{movie.Id}", new { status = "success", data = movie }); + } + + + // Get all Movies + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAllMovies(IRepository repository) + { + var movies = await repository.GetAllAsync(); + return TypedResults.Ok(new { status = "success", data = movies }); + } + + // Update a Movie + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task UpdateMovie(int id, Movie movie, IRepository repository) + { + var existingMovie = await repository.GetByIdAsync(id); + if (existingMovie == null) + return TypedResults.NotFound(); + + existingMovie.Title = movie.Title; + existingMovie.Rating = movie.Rating; + existingMovie.Description = movie.Description; + existingMovie.RuntimeMins = movie.RuntimeMins; + + await repository.UpdateAsync(existingMovie); + return TypedResults.Created($"/movies/{id}", new { status = "success", data = existingMovie }); + } + + // Delete a Movie + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task DeleteMovie(int id, IRepository repository) + { + var movie = await repository.GetByIdAsync(id); + if (movie == null) + return TypedResults.NotFound(); + + await repository.DeleteAsync(id); + return TypedResults.Ok(new { status = "success", data = movie }); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoints.cs new file mode 100644 index 00000000..5666c01a --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/ScreeningEndpoints.cs @@ -0,0 +1,36 @@ +using api_cinema_challenge.Models; +using api_cinema_challenge.Repositories.SpecificRepositories; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Endpoints +{ + public static class ScreeningEndpoints + { + + public static void ConfigureScreeningApi(this WebApplication app) + { + + var screenings = app.MapGroup("/movies/{movieId}/screenings"); + screenings.MapPost("/", CreateScreening); + screenings.MapGet("/", GetAllScreeningsForMovie); + } + + // Create a Screening for a Movie + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task CreateScreening(int movieId, Screening screening, IScreeningRepository screeningRepository) + { + screening.MovieId = movieId; + await screeningRepository.AddAsync(screening); + return TypedResults.Created($"/movies/{movieId}/screenings/{screening.Id}", new { status = "success", data = screening }); + } + + // Get all Screenings for a Movie + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAllScreeningsForMovie(int movieId, IScreeningRepository screeningRepository) + { + var screenings = await screeningRepository.GetScreeningsByMovieAsync(movieId); + return TypedResults.Ok(new { status = "success", data = screenings }); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoints.cs b/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoints.cs new file mode 100644 index 00000000..2d6d2fc9 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Endpoints/TicketEndpoints.cs @@ -0,0 +1,36 @@ +using api_cinema_challenge.Models; +using api_cinema_challenge.Repositories.SpecificRepositories; +using Microsoft.AspNetCore.Mvc; + +namespace api_cinema_challenge.Endpoints +{ + public static class TicketEndpoints + { + public static void ConfigureTicketApi(this WebApplication app) + { + var tickets = app.MapGroup("/customers/{customerId}/screenings/{screeningId}"); + + tickets.MapPost("/", BookTicket); + tickets.MapGet("/", GetAllTicketsForCustomer); + } + + // Book a Ticket for a Customer and Screening + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task BookTicket(int customerId, int screeningId, Ticket ticket, ITicketRepository ticketRepository) + { + ticket.CustomerId = customerId; + ticket.ScreeningId = screeningId; + await ticketRepository.AddAsync(ticket); + return TypedResults.Created($"/customers/{customerId}/screenings/{screeningId}", new { status = "success", data = ticket }); + } + + // Get all Tickets for a Customer and Screening + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetAllTicketsForCustomer(int customerId, int screeningId, ITicketRepository ticketRepository) + { + var tickets = await ticketRepository.GetTicketsByCustomerAsync(customerId); + return TypedResults.Ok(new { status = "success", data = tickets }); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250128091641_InitialMigration.Designer.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250128091641_InitialMigration.Designer.cs new file mode 100644 index 00000000..20766ded --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250128091641_InitialMigration.Designer.cs @@ -0,0 +1,227 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api_cinema_challenge.Data; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + [DbContext(typeof(CinemaContext))] + [Migration("20250128091641_InitialMigration")] + partial class InitialMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text") + .HasColumnName("phone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("rating"); + + b.Property("RuntimeMins") + .HasColumnType("integer") + .HasColumnName("runtime_minutes"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer") + .HasColumnName("capacity"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("MovieId") + .HasColumnType("integer") + .HasColumnName("movie_id"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screen_number"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("starts_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("NumSeats") + .HasColumnType("integer") + .HasColumnName("num_seats"); + + b.Property("ScreeningId") + .HasColumnType("integer") + .HasColumnName("screening_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ScreeningId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.HasOne("api_cinema_challenge.Models.Movie", "Movie") + .WithMany("Screenings") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.HasOne("api_cinema_challenge.Models.Customer", "Customer") + .WithMany("Tickets") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api_cinema_challenge.Models.Screening", "Screening") + .WithMany("Tickets") + .HasForeignKey("ScreeningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Screening"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Navigation("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/20250128091641_InitialMigration.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/20250128091641_InitialMigration.cs new file mode 100644 index 00000000..83c74b86 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/20250128091641_InitialMigration.cs @@ -0,0 +1,135 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + /// + public partial class InitialMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + name = table.Column(type: "text", nullable: false), + email = table.Column(type: "text", nullable: false), + phone = table.Column(type: "text", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Movies", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + title = table.Column(type: "text", nullable: false), + rating = table.Column(type: "text", nullable: false), + description = table.Column(type: "text", nullable: false), + runtime_minutes = table.Column(type: "integer", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Movies", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Screenings", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + movie_id = table.Column(type: "integer", nullable: false), + screen_number = table.Column(type: "integer", nullable: false), + capacity = table.Column(type: "integer", nullable: false), + starts_at = table.Column(type: "timestamp with time zone", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Screenings", x => x.Id); + table.ForeignKey( + name: "FK_Screenings_Movies_movie_id", + column: x => x.movie_id, + principalTable: "Movies", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Tickets", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + customer_id = table.Column(type: "integer", nullable: false), + screening_id = table.Column(type: "integer", nullable: false), + num_seats = table.Column(type: "integer", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tickets", x => x.Id); + table.ForeignKey( + name: "FK_Tickets_Customers_customer_id", + column: x => x.customer_id, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Tickets_Screenings_screening_id", + column: x => x.screening_id, + principalTable: "Screenings", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Screenings_movie_id", + table: "Screenings", + column: "movie_id"); + + migrationBuilder.CreateIndex( + name: "IX_Tickets_customer_id", + table: "Tickets", + column: "customer_id"); + + migrationBuilder.CreateIndex( + name: "IX_Tickets_screening_id", + table: "Tickets", + column: "screening_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Tickets"); + + migrationBuilder.DropTable( + name: "Customers"); + + migrationBuilder.DropTable( + name: "Screenings"); + + migrationBuilder.DropTable( + name: "Movies"); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs new file mode 100644 index 00000000..f5a950f5 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Migrations/CinemaContextModelSnapshot.cs @@ -0,0 +1,224 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api_cinema_challenge.Data; + +#nullable disable + +namespace api_cinema_challenge.Migrations +{ + [DbContext(typeof(CinemaContext))] + partial class CinemaContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text") + .HasColumnName("phone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Rating") + .IsRequired() + .HasColumnType("text") + .HasColumnName("rating"); + + b.Property("RuntimeMins") + .HasColumnType("integer") + .HasColumnName("runtime_minutes"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer") + .HasColumnName("capacity"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("MovieId") + .HasColumnType("integer") + .HasColumnName("movie_id"); + + b.Property("ScreenNumber") + .HasColumnType("integer") + .HasColumnName("screen_number"); + + b.Property("StartsAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("starts_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("NumSeats") + .HasColumnType("integer") + .HasColumnName("num_seats"); + + b.Property("ScreeningId") + .HasColumnType("integer") + .HasColumnName("screening_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ScreeningId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.HasOne("api_cinema_challenge.Models.Movie", "Movie") + .WithMany("Screenings") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Ticket", b => + { + b.HasOne("api_cinema_challenge.Models.Customer", "Customer") + .WithMany("Tickets") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("api_cinema_challenge.Models.Screening", "Screening") + .WithMany("Tickets") + .HasForeignKey("ScreeningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Screening"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Movie", b => + { + b.Navigation("Screenings"); + }); + + modelBuilder.Entity("api_cinema_challenge.Models.Screening", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs new file mode 100644 index 00000000..94270da3 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Customer.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace api_cinema_challenge.Models +{ + public class Customer + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] // Auto-increment + public int Id { get; set; } + + [Required] + [Column("name")] + public string Name { get; set; } + + [Required] + [Column("email")] + public string Email { get; set; } + + [Required] + [Column("phone")] + public string Phone { get; set; } + + [Column("created_at")] + public DateTime CreatedAt { get; set; } + + [Column("updated_at")] + public DateTime UpdatedAt { get; set; } + + [JsonIgnore] + public List Tickets { get; set; } // One-to-Many with Tickets + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs new file mode 100644 index 00000000..bd2e6410 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Movie.cs @@ -0,0 +1,38 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace api_cinema_challenge.Models +{ + public class Movie + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + [Required] + [Column("title")] + public string Title { get; set; } + + [Required] + [Column("rating")] + public string Rating { get; set; } + + [Required] + [Column("description")] + public string Description { get; set; } + + [Required] + [Column("runtime_minutes")] + public int RuntimeMins { get; set; } + + [Column("created_at")] + public DateTime CreatedAt { get; set; } + + [Column("updated_at")] + public DateTime UpdatedAt { get; set; } + + [JsonIgnore] + public List Screenings { get; set; } // One-to-Many with Screenings + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs new file mode 100644 index 00000000..39f3af54 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Screening.cs @@ -0,0 +1,40 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace api_cinema_challenge.Models +{ + public class Screening + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + [ForeignKey("Movie")] + [Column("movie_id")] + public int MovieId { get; set; } + + [Required] + [Column("screen_number")] + public int ScreenNumber { get; set; } + + [Required] + [Column("capacity")] + public int Capacity { get; set; } + + [Required] + [Column("starts_at")] + public DateTime StartsAt { get; set; } + + [Column("created_at")] + public DateTime CreatedAt { get; set; } + + [Column("updated_at")] + public DateTime UpdatedAt { get; set; } + + public Movie Movie { get; set; } // Navigation property for Movie + + [JsonIgnore] + public List Tickets { get; set; } // One-to-Many with Tickets + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs new file mode 100644 index 00000000..96de6d4e --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Models/Ticket.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace api_cinema_challenge.Models +{ + public class Ticket + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + [ForeignKey("Customer")] + [Column("customer_id")] + public int CustomerId { get; set; } + + [ForeignKey("Screening")] + [Column("screening_id")] + public int ScreeningId { get; set; } + + [Required] + [Column("num_seats")] + public int NumSeats { get; set; } + + [Column("created_at")] + public DateTime CreatedAt { get; set; } + + [Column("updated_at")] + public DateTime UpdatedAt { get; set; } + + public Customer Customer { get; set; } // Navigation property for Customer + public Screening Screening { get; set; } // Navigation property for Screening + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Program.cs b/api-cinema-challenge/api-cinema-challenge/Program.cs index e55d9d54..3aa7e0bd 100644 --- a/api-cinema-challenge/api-cinema-challenge/Program.cs +++ b/api-cinema-challenge/api-cinema-challenge/Program.cs @@ -1,4 +1,8 @@ using api_cinema_challenge.Data; +using api_cinema_challenge.Endpoints; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repositories.GenericRepositories; +using api_cinema_challenge.Repositories.SpecificRepositories; var builder = WebApplication.CreateBuilder(args); @@ -7,8 +11,18 @@ builder.Services.AddSwaggerGen(); builder.Services.AddDbContext(); +builder.Services.AddControllers(); +builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + var app = builder.Build(); +await app.SeedCinemaData(); + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { @@ -17,4 +31,12 @@ } app.UseHttpsRedirection(); +app.UseAuthorization(); + +app.MapControllers(); + +app.ConfigureCustomerApi(); +app.ConfigureMovieApi(); +app.ConfigureScreeningApi(); +app.ConfigureTicketApi(); app.Run(); diff --git a/api-cinema-challenge/api-cinema-challenge/Repositories/GenericRepositories/IRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repositories/GenericRepositories/IRepository.cs new file mode 100644 index 00000000..c417fe54 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repositories/GenericRepositories/IRepository.cs @@ -0,0 +1,11 @@ +namespace api_cinema_challenge.Repositories.GenericRepositories +{ + public interface IRepository where T : class + { + Task GetByIdAsync(int id); + Task> GetAllAsync(); + Task AddAsync(T entity); + Task UpdateAsync(T entity); + Task DeleteAsync(int id); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repositories/GenericRepositories/Repository.cs b/api-cinema-challenge/api-cinema-challenge/Repositories/GenericRepositories/Repository.cs new file mode 100644 index 00000000..ed91b0e8 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repositories/GenericRepositories/Repository.cs @@ -0,0 +1,50 @@ +using api_cinema_challenge.Data; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Repositories.GenericRepositories +{ + public class Repository : IRepository where T : class + { + protected readonly CinemaContext _context; + protected readonly DbSet _dbSet; + + public Repository(CinemaContext context) + { + _context = context; + _dbSet = _context.Set(); + } + + public async Task AddAsync(T entity) + { + await _dbSet.AddAsync(entity); + await _context.SaveChangesAsync(); + } + + public async Task> GetAllAsync() + { + return await _dbSet.ToListAsync(); + } + + public async Task GetByIdAsync(int id) + { + return await _dbSet.FindAsync(id); + } + + public async Task UpdateAsync(T entity) + { + _dbSet.Update(entity); + await _context.SaveChangesAsync(); + } + + public async Task DeleteAsync(int id) + { + var entity = await _dbSet.FindAsync(id); + if (entity != null) + { + _dbSet.Remove(entity); + await _context.SaveChangesAsync(); + } + } + } +} + diff --git a/api-cinema-challenge/api-cinema-challenge/Repositories/SpecificRepositories/IScreeningRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repositories/SpecificRepositories/IScreeningRepository.cs new file mode 100644 index 00000000..4afff9bf --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repositories/SpecificRepositories/IScreeningRepository.cs @@ -0,0 +1,10 @@ +using api_cinema_challenge.Models; +using api_cinema_challenge.Repositories.GenericRepositories; + +namespace api_cinema_challenge.Repositories.SpecificRepositories +{ + public interface IScreeningRepository : IRepository + { + Task> GetScreeningsByMovieAsync(int movieId); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repositories/SpecificRepositories/ITicketRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repositories/SpecificRepositories/ITicketRepository.cs new file mode 100644 index 00000000..9f7ceb80 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repositories/SpecificRepositories/ITicketRepository.cs @@ -0,0 +1,12 @@ +using System.Net.Sockets; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repositories.GenericRepositories; + +namespace api_cinema_challenge.Repositories.SpecificRepositories +{ + public interface ITicketRepository : IRepository + { + Task> GetTicketsByCustomerAsync(int customerId); + Task> GetTicketsByScreeningAsync(int screeningId); + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repositories/SpecificRepositories/ScreeningRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repositories/SpecificRepositories/ScreeningRepository.cs new file mode 100644 index 00000000..e00e9ae4 --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repositories/SpecificRepositories/ScreeningRepository.cs @@ -0,0 +1,26 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repositories.GenericRepositories; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Repositories.SpecificRepositories +{ + public class ScreeningRepository : Repository, IScreeningRepository + { + private readonly CinemaContext _context; + + public ScreeningRepository(CinemaContext dbContext) : base(dbContext) + { + + _context = dbContext; + } + + public async Task> GetScreeningsByMovieAsync(int movieId) + { + return await _context.Set() + .Where(s => s.MovieId == movieId) + .Include(s => s.Movie) + .ToListAsync(); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/Repositories/SpecificRepositories/TicketRepository.cs b/api-cinema-challenge/api-cinema-challenge/Repositories/SpecificRepositories/TicketRepository.cs new file mode 100644 index 00000000..25b6d4ea --- /dev/null +++ b/api-cinema-challenge/api-cinema-challenge/Repositories/SpecificRepositories/TicketRepository.cs @@ -0,0 +1,35 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repositories; +using api_cinema_challenge.Repositories.GenericRepositories; +using Microsoft.EntityFrameworkCore; +using System.Net.Sockets; + +namespace api_cinema_challenge.Repositories.SpecificRepositories +{ + public class TicketRepository : Repository, ITicketRepository + { + + private readonly CinemaContext _context; + + public TicketRepository(CinemaContext dbContext) : base(dbContext) { + + _context = dbContext; + } + + public async Task> GetTicketsByCustomerAsync(int customerId) + { + return await _context.Set() + .Where(t => t.CustomerId == customerId) + .Include(t => t.Customer) + .Include(t => t.Screening) + .ThenInclude(s => s.Movie) + .ToListAsync(); + } + + public async Task> GetTicketsByScreeningAsync(int screeningId) + { + return await _context.Set().Where(t => t.ScreeningId == screeningId).ToListAsync(); + } + } +} diff --git a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj index 8c888bf8..89f468ed 100644 --- a/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj +++ b/api-cinema-challenge/api-cinema-challenge/api-cinema-challenge.csproj @@ -1,4 +1,4 @@ - + net9.0 @@ -27,8 +27,4 @@ - - - -