diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c2b29e --- /dev/null +++ b/.gitignore @@ -0,0 +1,393 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Nuget personal access tokens and Credentials +nuget.config + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.idea/ +*.sln.iml + +# sqlite +*.db +*.db-wal +*.db-shm \ No newline at end of file diff --git a/Films/.dockerignore b/Films/.dockerignore new file mode 100644 index 0000000..cd967fc --- /dev/null +++ b/Films/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/Films/Authentication.Shared/Authentication.Shared.csproj b/Films/Authentication.Shared/Authentication.Shared.csproj new file mode 100644 index 0000000..c52e1b0 --- /dev/null +++ b/Films/Authentication.Shared/Authentication.Shared.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/Films/Authentication.Shared/JwtAuthenticationOptions.cs b/Films/Authentication.Shared/JwtAuthenticationOptions.cs new file mode 100644 index 0000000..fd1b38f --- /dev/null +++ b/Films/Authentication.Shared/JwtAuthenticationOptions.cs @@ -0,0 +1,68 @@ +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace Authentication.Shared; + +/// +/// Represents authorization options +/// +public class JwtAuthenticationOptions +{ + /// + /// JwtAuthenticationOptions constructor + /// + /// Token publisher + /// Token consumer + /// Encryption key + /// Access token lifetime(in minutes) + /// Refresh token lifetime(in minutes) + public JwtAuthenticationOptions(string issuer, string audience, string key, int accessLifetime, int refreshLifetime) + { + Issuer = issuer; + Audience = audience; + Key = key; + AccessLifetime = accessLifetime; + RefreshLifetime = refreshLifetime; + } + + /// + /// JwtAuthenticationOptions constructor + /// + public JwtAuthenticationOptions() + { + } + + /// + /// Token publisher + /// + public string Issuer { get; set; } = ""; + + /// + /// Token consumer + /// + public string Audience { get; set; } = ""; + + /// + /// Encryption key + /// + public string Key { get; set; } = ""; + + /// + /// Access token lifetime(in minutes) + /// + public int AccessLifetime { get; set; } + + /// + /// Refresh token lifetime(in minutes) + /// + public int RefreshLifetime { get; set; } + + /// + /// Method for getting symmetric security key + /// + /// Symmetric security key + public SymmetricSecurityKey GetSymmetricSecurityKey(string key) + { + return new SymmetricSecurityKey(Encoding.ASCII.GetBytes(key)); + } +} \ No newline at end of file diff --git a/Films/Films.API/Controllers/FilmController.cs b/Films/Films.API/Controllers/FilmController.cs new file mode 100644 index 0000000..5828c67 --- /dev/null +++ b/Films/Films.API/Controllers/FilmController.cs @@ -0,0 +1,158 @@ +using Films.Core.Services; +using Films.Domain.Models; +using Films.DTOs; +using Kirel.Repositories.Core.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Films.API.Controllers; + +/// +/// Controller for handling film-related operations. +/// +[ApiController] +[Route("api/films")] +public class FilmController : ControllerBase +{ + private readonly FilmService _filmService; + + /// + /// Initializes a new instance of the class. + /// + /// The film service to retrieve and manage film data. + public FilmController(FilmService filmService) + { + _filmService = filmService; + } + + /// + /// Searches for films by name. + /// + /// The name of the film to search for. + /// Returns a list of films matching the search criteria. + [HttpGet("search")] + public async Task>> SearchFilms(string filmName) + { + try + { + var films = await _filmService.SearchFilms(filmName); + return Ok(films); + } + catch (FilmService.FilmNotFoundException ex) + { + return NotFound(ex.Message); + } + catch (Exception) + { + // If an unexpected exception occurs return a 500 Internal Server Error response. + return StatusCode(500, "An error occurred while processing your request."); + } + } + + /// + /// Retrieves a paginated list of films based on the specified parameters. + /// + /// Page number of the paginated results. + /// Number of items per page. + /// Field by which the results should be ordered. + /// Sorting direction (ascending or descending). + /// Search term to filter the results. + /// + /// Paginated result containing a list of FilmDto objects. + [HttpGet("all")] + public async Task>>> GetAllFilms( + [FromQuery] int pageNumber = 1, + [FromQuery] int pageSize = 10, + [FromQuery] string? orderBy = "", + [FromQuery] string orderDirection = "asc", + [FromQuery] string? search = "", + [FromQuery] List? genreIds = null) + { + + SortDirection directionEnum; + if (orderDirection == "asc") + { + directionEnum = SortDirection.Asc; + } + else + { + directionEnum = SortDirection.Desc; + } + + var result = await _filmService.GetAllFilmsPaginated( + pageNumber, pageSize, orderBy!, directionEnum, search!, genreIds); + + return Ok(result); + + } + + + /// Deletes a film by ID. + /// The ID of the film to delete. + /// Returns a status indicating the success of the delete operation. + [HttpDelete("{filmId}")] + public async Task DeleteFilm(int filmId) + { + try + { + await _filmService.DeleteFilm(filmId); + return Ok(); + } + catch (FilmService.FilmNotFoundException ex) + { + return NotFound(ex.Message); + } + catch (Exception) + { + return StatusCode(500, "An error occurred while processing your request."); + } + } + + /// + /// Creates a new film. + /// + /// The DTO containing the film information to create. + /// Returns the ID of the created film. + [HttpPost] + public async Task CreateFilm([FromBody] FilmCreateDto filmCreateDto) + { + if (!ModelState.IsValid) return BadRequest(ModelState); + + await _filmService.CreateFilm(filmCreateDto); + + return Ok(); + } + /// + /// Get all films by genres + /// + /// Ids of genres you need + /// + [HttpGet("films/by-genre-ids")] + public async Task GetFilmsByGenreIds([FromQuery] List genreIds) + { + var filmDtos = await _filmService.GetFilmsByGenreIds(genreIds); + return Ok(filmDtos); + } + /// + /// Updates a film by ID. + /// + /// The ID of the film to update. + /// The DTO containing the updated film information. + /// Returns a status indicating the success of the update operation. + [HttpPut("update/{filmId}")] + public async Task UpdateFilm(int filmId, FilmUpdateDto filmDto) + { + try + { + await _filmService.UpdateFilm(filmId, filmDto); + return Ok(); + } + catch (FilmService.FilmNotFoundException ex) + { + return NotFound(ex.Message); + } + catch (Exception) + { + return StatusCode(500, "An error occurred while processing your request."); + } + } +} \ No newline at end of file diff --git a/Films/Films.API/Dockerfile b/Films/Films.API/Dockerfile new file mode 100644 index 0000000..37ab475 --- /dev/null +++ b/Films/Films.API/Dockerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["Films.API/Films.API.csproj", "Films.API/"] +RUN dotnet restore "Films.API/Films.API.csproj" +COPY . . +WORKDIR "/src/Films.API" +RUN dotnet build "Films.API.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Films.API.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Films.API.dll"] diff --git a/Films/Films.API/Extensions/AuthenticationExtension.cs b/Films/Films.API/Extensions/AuthenticationExtension.cs new file mode 100644 index 0000000..36747e4 --- /dev/null +++ b/Films/Films.API/Extensions/AuthenticationExtension.cs @@ -0,0 +1,41 @@ +using Authentication.Shared; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; + +namespace Films.API.Extensions; + +/// +/// Authentication configuration extension +/// +public static class AuthenticationExtension +{ + /// + /// Add authentication configuration to DI + /// + /// services collection + /// JWT Token generation config + public static void AddAuthenticationConfigurations(this IServiceCollection services, + JwtAuthenticationOptions authOptions) + { + services.AddAuthentication(option => + { + // Fixing 404 error when adding an attribute Authorize to controller + option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.RequireHttpsMetadata = false; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = authOptions.Issuer, + ValidateAudience = true, + ValidAudience = authOptions.Audience, + ValidateLifetime = true, + IssuerSigningKey = authOptions.GetSymmetricSecurityKey(authOptions.Key), + ValidateIssuerSigningKey = true + }; + }); + } +} \ No newline at end of file diff --git a/Films/Films.API/Extensions/SwaggerExtension.cs b/Films/Films.API/Extensions/SwaggerExtension.cs new file mode 100644 index 0000000..fe47ace --- /dev/null +++ b/Films/Films.API/Extensions/SwaggerExtension.cs @@ -0,0 +1,54 @@ +using Microsoft.OpenApi.Models; + +namespace Films.API.Extensions; + +/// +/// Extension that adds swagger documentation support +/// +public static class SwaggerExtension +{ + /// + /// Add swagger documentation stuff to DI + /// + /// + public static void AddSwagger(this IServiceCollection services) + { + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + services.AddEndpointsApiExplorer(); + // Configure swagger + services.AddSwaggerGen(c => + { + //Add swagger xml docs + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Films Api", Version = "v1" }); + //Set the comments path for the swagger json and ui. + var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml", SearchOption.TopDirectoryOnly) + .ToList(); + xmlFiles.ForEach(xmlFile => c.IncludeXmlComments(xmlFile)); + // Add JWT token authorization support + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Name = "Authorization", + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = + "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\"" + }); + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] { } + } + }); + }); + } +} \ No newline at end of file diff --git a/Films/Films.API/Films.API.csproj b/Films/Films.API/Films.API.csproj new file mode 100644 index 0000000..a7f9c0a --- /dev/null +++ b/Films/Films.API/Films.API.csproj @@ -0,0 +1,37 @@ + + + + net6.0 + enable + enable + Linux + true + true + + + + + + + + + + + + + + + + + + .dockerignore + + + + + + + + + + diff --git a/Films/Films.API/Program.cs b/Films/Films.API/Program.cs new file mode 100644 index 0000000..de2e80e --- /dev/null +++ b/Films/Films.API/Program.cs @@ -0,0 +1,59 @@ +using System.Reflection; +using Authentication.Shared; +using Films.API.Extensions; +using Films.Core.Extensions; +using Films.Infrastructure; +using Films.Infrastructure.Extentions; +using FluentValidation; +using FluentValidation.AspNetCore; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +var jwtOptions = builder.Configuration.GetSection("JwtAuthenticationOptions").Get(); + +//taking connection string from appsettings.json +var connectionString = builder.Configuration.GetConnectionString("SqlConnection"); +builder.Services.AddDbContext(options => + options.UseSqlServer(connectionString)); + +// Add services to the container. +builder.Services.AddFilmsServices(); +builder.Services.AddFilmsRepositories(); + + +//Add AutoMapper. Class <--> Dto mappings. Configured in Mappings. +builder.Services.AddMapper(); + +/*builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly());*/ +builder.Services.AddFluentValidationAutoValidation(); +builder.Services.AddFluentValidationClientsideAdapters(); +builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); +//Add authentication +builder.Services.AddAuthentication(); +builder.Services.AddAuthenticationConfigurations(jwtOptions); + +// Configure swagger +builder.Services.AddSwagger(); +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); +FilmDbInitialize.Initialize(app.Services.GetRequiredService().CreateScope().ServiceProvider); +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/Films/Films.API/Properties/launchSettings.json b/Films/Films.API/Properties/launchSettings.json new file mode 100644 index 0000000..af866fb --- /dev/null +++ b/Films/Films.API/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:10057", + "sslPort": 44332 + } + }, + "profiles": { + "Films.API": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7016;http://localhost:5261", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Films/Films.API/appsettings.Development.json b/Films/Films.API/appsettings.Development.json new file mode 100644 index 0000000..341f582 --- /dev/null +++ b/Films/Films.API/appsettings.Development.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "JwtAuthenticationOptions": { + "Issuer": "ExampleServer", + "Audience": "ExampleClient", + "Key": "SomeSuperSecretKey123", + "AccessLifetime": 20, + "RefreshLifetime": 3600 + } +} diff --git a/Films/Films.API/appsettings.json b/Films/Films.API/appsettings.json new file mode 100644 index 0000000..f1e9f83 --- /dev/null +++ b/Films/Films.API/appsettings.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "PostgreConnection": "Host=localhost;Port=5432;Database=Films;Username=lsodis;Password=6742", + "SqlConnection": "Server=localhost;Database=Films;Trusted_Connection=True;Encrypt=False;" + }, + "AllowedHosts": "*", + "JwtAuthenticationOptions": { + "Issuer": "ExampleServer", + "Audience": "ExampleClient", + "Key": "SomeSuperSecretKey123", + "AccessLifetime": 20, + "RefreshLifetime": 3600 + } +} diff --git a/Films/Films.Core/Extensions/AutoMapperExtension.cs b/Films/Films.Core/Extensions/AutoMapperExtension.cs new file mode 100644 index 0000000..f51e7d6 --- /dev/null +++ b/Films/Films.Core/Extensions/AutoMapperExtension.cs @@ -0,0 +1,17 @@ +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; + +namespace Films.Core.Extensions; + +/// +/// +public static class AutoMapperExtension +{ + /// + /// + /// + public static void AddMapper(this IServiceCollection services) + { + services.AddAutoMapper(Assembly.GetExecutingAssembly()); + } +} \ No newline at end of file diff --git a/Films/Films.Core/Extensions/FilmsServicesExtension.cs b/Films/Films.Core/Extensions/FilmsServicesExtension.cs new file mode 100644 index 0000000..9b0dac9 --- /dev/null +++ b/Films/Films.Core/Extensions/FilmsServicesExtension.cs @@ -0,0 +1,17 @@ +using Films.Core.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Films.Core.Extensions; + +/// +/// +public static class FilmsServicesExtension +{ + /// + /// + /// + public static void AddFilmsServices(this IServiceCollection services) + { + services.AddScoped(); + } +} \ No newline at end of file diff --git a/Films/Films.Core/Films.Core.csproj b/Films/Films.Core/Films.Core.csproj new file mode 100644 index 0000000..82c6359 --- /dev/null +++ b/Films/Films.Core/Films.Core.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + enable + true + true + + + + + + + + + + + + + + + + + + + diff --git a/Films/Films.Core/Mappers/FilmMapper.cs b/Films/Films.Core/Mappers/FilmMapper.cs new file mode 100644 index 0000000..7fb0639 --- /dev/null +++ b/Films/Films.Core/Mappers/FilmMapper.cs @@ -0,0 +1,22 @@ +using AutoMapper; +using Films.Domain.Models; +using Films.DTOs; + +namespace Films.Core.Mappers; + +/// +/// Mapping Film for FilmDTO +/// +public class FilmMapper : Profile +{ + /// + /// Films constructor + /// + public FilmMapper() + { + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + } +} \ No newline at end of file diff --git a/Films/Films.Core/Mappers/GenreMapper.cs b/Films/Films.Core/Mappers/GenreMapper.cs new file mode 100644 index 0000000..6b6d592 --- /dev/null +++ b/Films/Films.Core/Mappers/GenreMapper.cs @@ -0,0 +1,21 @@ +using AutoMapper; +using Films.Domain.Models; +using Films.DTOs; + +namespace Films.Core.Mappers; + +/// +/// Mapping Genre for GenreDTO +/// +public class GenreMapper : Profile +{ + /// + /// GenreMapping constructor + /// + public GenreMapper() + { + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + } +} \ No newline at end of file diff --git a/Films/Films.Core/Services/FilmService.cs b/Films/Films.Core/Services/FilmService.cs new file mode 100644 index 0000000..c5630ba --- /dev/null +++ b/Films/Films.Core/Services/FilmService.cs @@ -0,0 +1,271 @@ +using System.Linq.Expressions; +using System.Transactions; +using AutoMapper; +using Films.Domain.Models; +using Films.DTOs; +using Kirel.Repositories.Core.Interfaces; +using Kirel.Repositories.Core.Models; +using Kirel.Shared; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Films.Core.Services; + +/// +/// Service responsible for film-related operations and logic. +/// +public class FilmService +{ + private readonly IKirelGenericEntityRepository _filmRepository; + private readonly IKirelGenericEntityRepository _genreRepository; + private readonly IMapper _mapper; + + + /// + /// Initializes a new instance of the class. + /// + /// The repository for accessing film data. + /// AutoMapper instance + /// The repository to accessing Genre data + public FilmService(IKirelGenericEntityRepository filmRepository, IMapper mapper, + IKirelGenericEntityRepository genreRepository) + { + _filmRepository = filmRepository; + _mapper = mapper; + _genreRepository = genreRepository; + + } + + /// + /// Searches for films in the database based on the provided film name. + /// + /// The name of the film to search for. + /// A list of FilmDto objects representing the matching films. + /// Thrown if no films matching the provided name are found. + public async Task> SearchFilms(string filmName) + { + // Search for films in the database based on the provided film name. + var existingFilms = await _filmRepository.GetList( + m => m.Name != null && m.Name.Contains(filmName), + includes: q => q.Include(f => f.Genres) + ); + if (existingFilms == null || !existingFilms.Any()) + throw new FilmNotFoundException($"Film with the title '{filmName}' was not found in the database."); + + // Map the existingFilms to FilmDto objects + var filmDtos = _mapper.Map>(existingFilms); + + return filmDtos; + } + + /// + /// Creates a new film in the database with the provided data and associated genres. + /// + /// Data for the new film. + /// An asynchronous task representing the operation execution. + public async Task CreateFilm(FilmCreateDto filmCreateDto) + { + var film = _mapper.Map(filmCreateDto); + + var existingGenres = await _genreRepository.GetList(orderBy: null, includes: null, page: 0, pageSize: 0); + + if (filmCreateDto.Genres != null) + { + var genresToRemove = film.Genres.ToList(); // Создаем список для жанров, которые нужно удалить + + foreach (var genreDto in filmCreateDto.Genres) + { + var existingGenre = existingGenres.FirstOrDefault(g => g.Name == genreDto.Name); + if (existingGenre != null) + { + // Прикрепляем существующий жанр к фильму + film.Genres.Add(existingGenre); + + // Если жанр существует, удаляем его из списка для удаления + genresToRemove.Remove(existingGenre); + } + else + { + // Создаем новый жанр, если он не существует + var newGenre = new Genre { Name = genreDto.Name }; + film.Genres.Add(newGenre); + } + } + + // Удаляем ненужные жанры из фильма + foreach (var genreToRemove in genresToRemove) + { + film.Genres.Remove(genreToRemove); + } + } + + // Сохраняем фильм в базе данных + await _filmRepository.Insert(film); + } + + /// + /// Searching all films which have this genre Id + /// + /// + /// + public async Task> GetFilmsByGenreIds(List genreIds) + { + var filmDtos = new List(); + + foreach (var genreId in genreIds) + { + var films = await _filmRepository.GetList( + f => f.Genres.Any(g => g.Id == genreId), + includes: q => q.Include(f => f.Genres) + ); + + var genreFilmDtos = _mapper.Map>(films); + filmDtos.AddRange(genreFilmDtos); + } + + return filmDtos; + } + + + + + /// + /// Updates an existing film. + /// + /// The ID of the film to update. + /// The DTO containing the updated film information. + public async Task UpdateFilm(int filmId, FilmUpdateDto filmDto) + { + // Retrieve the existing film by its ID. + var existingFilm = await _filmRepository.GetById(filmId); + if (existingFilm == null) + throw new FilmNotFoundException($"Film with ID '{filmId}' was not found in the database."); + + // Update the existing film entity with the new data from the DTO. + _mapper.Map(filmDto, existingFilm); + + // Perform the update in the repository. + await _filmRepository.Update(existingFilm); + } + + /// + /// Retrieves a paginated list of all films in the database. + /// + /// Page number of the paginated results. + /// Number of items per page. + /// Field by which the results should be ordered. + /// Sorting direction (ascending or descending). + /// Search term to filter the results. + /// Id of genre which you want to search for + /// Paginated result containing a list of FilmDto objects. + public async Task>> GetAllFilmsPaginated( + int pageNumber = 0, int pageSize = 0, + string orderBy = "", SortDirection orderDirection = SortDirection.Asc, + string search = "", List? genreIds = null) + { + Expression> expression = null!; + if (!string.IsNullOrWhiteSpace(search)) + { + Expression> searchExpression = PredicateBuilder.PredicateSearchInAllFields(search); + expression = searchExpression; + } + + if (genreIds != null && genreIds.Any()) + { + Expression> genreExpression = f => f.Genres.Any(g => genreIds.Contains(g.Id)); + if (expression == null) + { + expression = genreExpression; + } + else + { + expression = PredicateBuilder.And(expression, genreExpression); + } + } + + var orderByDelegate = GenerateOrderingMethod(orderBy, orderDirection); + var includesDelegate = GenerateIncludes(); + + var totalCount = await _filmRepository.Count(expression); + + var pagination = Pagination.Generate(pageNumber, pageSize, totalCount); + + var films = await _filmRepository.GetList(expression, orderByDelegate, includesDelegate, pagination.CurrentPage, + pagination.PageSize); + + var filmsDto = _mapper.Map>(films); + + var result = new PaginatedResult> + { + Pagination = pagination, + Data = filmsDto + }; + return result; + } + + + + + /// + /// Deletes a film from the database. + /// + /// The ID of the film to delete. + public async Task DeleteFilm(int filmId) + { + // Retrieve the film by its ID. + var film = await _filmRepository.GetById(filmId); + + if (film == null) throw new FilmNotFoundException($"Film with ID '{filmId}' was not found in the database."); + + // Delete the film from the repository. + await _filmRepository.Delete(filmId); + } + + + private Func, IOrderedQueryable> GenerateOrderingMethod(string orderBy, + SortDirection orderDirection) + { + Func, IOrderedQueryable> orderingMethod = null!; + if (string.IsNullOrEmpty(orderBy)) return orderingMethod!; + var orderExpression = PredicateBuilder.ToLambda(orderBy); + if (orderExpression == null) return orderingMethod!; + switch (orderDirection) + { + case SortDirection.Asc: + orderingMethod = o => o.OrderBy(orderExpression); + break; + case SortDirection.Desc: + orderingMethod = o => o.OrderByDescending(orderExpression); + break; + } + + return orderingMethod!; + } + + // Генерация делегата для включения связанных данных + private static Func, IQueryable> GenerateIncludes() + { + Func, IQueryable> includesDelegate = query => query.Include(f => f.Genres); + + includesDelegate = query => query.Include(f => f.Genres); + // Add other includes for different entity types as needed. + + return includesDelegate!; + } + + + /// + /// Custom exception class for indicating that a film was not found. + /// + public class FilmNotFoundException : Exception + { + /// + /// Retrieves exception message + /// + /// + public FilmNotFoundException(string message) : base(message) + { + } + } +} diff --git a/Films/Films.DTOs/FilmCreateDto.cs b/Films/Films.DTOs/FilmCreateDto.cs new file mode 100644 index 0000000..ab2ad89 --- /dev/null +++ b/Films/Films.DTOs/FilmCreateDto.cs @@ -0,0 +1,37 @@ +namespace Films.DTOs; + +/// +/// Film CreateDTO +/// +public class FilmCreateDto +{ + /// + /// Gets or sets the name of the film. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the rating of the film. + /// + public int Rating { get; set; } + + /// + /// Gets or sets the description of the film. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the list of genres associated with the film. + /// + public List? Genres { get; set; } + + /// + /// Gets or sets the URL of the film's poster. + /// + public string? PosterUrl { get; set; } + + /// + /// Gets or sets the timestamp when the film was created. + /// + public DateTime Created { get; set; } +} \ No newline at end of file diff --git a/Films/Films.DTOs/FilmDto.cs b/Films/Films.DTOs/FilmDto.cs new file mode 100644 index 0000000..d9481e5 --- /dev/null +++ b/Films/Films.DTOs/FilmDto.cs @@ -0,0 +1,42 @@ +namespace Films.DTOs; + +/// +/// Data transfer object (DTO) representing film details. +/// +public class FilmDto +{ + /// + /// Gets or sets the unique identifier for the film. + /// + public int Id { get; set; } + + /// + /// Gets or sets the name of the film. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the rating of the film. + /// + public int Rating { get; set; } + + /// + /// Gets or sets the description of the film. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the list of genres associated with the film. + /// + public List? Genres { get; set; } + + /// + /// Gets or sets the URL of the film's poster. + /// + public string? PosterURL { get; set; } + + /// + /// Gets or sets the timestamp when the film was created. + /// + public DateTime Created { get; set; } +} \ No newline at end of file diff --git a/Films/Films.DTOs/FilmUpdateDto.cs b/Films/Films.DTOs/FilmUpdateDto.cs new file mode 100644 index 0000000..8ae18a0 --- /dev/null +++ b/Films/Films.DTOs/FilmUpdateDto.cs @@ -0,0 +1,37 @@ +namespace Films.DTOs; + +/// +/// Film UpdateDto +/// +public class FilmUpdateDto +{ + /// + /// Gets or sets the name of the film. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the rating of the film. + /// + public int Rating { get; set; } + + /// + /// Gets or sets the description of the film. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the list of genres associated with the film. + /// + public List? Genres { get; set; } + + /// + /// Gets or sets the URL of the film's poster. + /// + public string? PosterUrl { get; set; } + + /// + /// Gets or sets the timestamp when the film was created. + /// + public DateTime Created { get; set; } +} \ No newline at end of file diff --git a/Films/Films.DTOs/Films.DTOs.csproj b/Films/Films.DTOs/Films.DTOs.csproj new file mode 100644 index 0000000..29e81bb --- /dev/null +++ b/Films/Films.DTOs/Films.DTOs.csproj @@ -0,0 +1,11 @@ + + + + net6.0 + enable + enable + true + true + + + diff --git a/Films/Films.DTOs/GenreCreateDto.cs b/Films/Films.DTOs/GenreCreateDto.cs new file mode 100644 index 0000000..3937251 --- /dev/null +++ b/Films/Films.DTOs/GenreCreateDto.cs @@ -0,0 +1,12 @@ +namespace Films.DTOs; + +/// +/// Data transfer object (DTO) representing a film genre. +/// +public class GenreCreateDto +{ + /// + /// Gets or sets the name of the genre. + /// + public string? Name { get; set; } +} \ No newline at end of file diff --git a/Films/Films.DTOs/GenreDto.cs b/Films/Films.DTOs/GenreDto.cs new file mode 100644 index 0000000..27bc477 --- /dev/null +++ b/Films/Films.DTOs/GenreDto.cs @@ -0,0 +1,22 @@ +namespace Films.DTOs; + +/// +/// Data transfer object (DTO) representing a film genre. +/// +public class GenreDto +{ + /// + /// Gets or sets the unique identifier for the genre. + /// + public int Id { get; set; } + + /// + /// Gets or sets the name of the genre. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the Created time of genre + /// + public DateTime Created { get; set; } +} \ No newline at end of file diff --git a/Films/Films.DTOs/GenreUpdateDto.cs b/Films/Films.DTOs/GenreUpdateDto.cs new file mode 100644 index 0000000..a9b2b42 --- /dev/null +++ b/Films/Films.DTOs/GenreUpdateDto.cs @@ -0,0 +1,17 @@ +namespace Films.DTOs; + +/// +/// Data transfer object (DTO) representing a film genre. +/// +public class GenreUpdateDto +{ + /// + /// Gets or sets the unique identifier for the genre. + /// + public int Id { get; set; } + + /// + /// Gets or sets the name of the genre. + /// + public string? Name { get; set; } +} \ No newline at end of file diff --git a/Films/Films.Domain/Films.Domain.csproj b/Films/Films.Domain/Films.Domain.csproj new file mode 100644 index 0000000..a53978b --- /dev/null +++ b/Films/Films.Domain/Films.Domain.csproj @@ -0,0 +1,16 @@ + + + + net6.0 + enable + enable + true + true + + + + + + + + diff --git a/Films/Films.Domain/Models/Film.cs b/Films/Films.Domain/Models/Film.cs new file mode 100644 index 0000000..c6b06d9 --- /dev/null +++ b/Films/Films.Domain/Models/Film.cs @@ -0,0 +1,47 @@ +using Kirel.Repositories.Core.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace Films.Domain.Models; + +/// +/// Represents a film entity with details like name, rating, description, and genres. +/// Implements interfaces for creation timestamp tracking and using an integer as the key. +/// + +public class Film : ICreatedAtTrackedEntity, IKeyEntity +{ + /// + /// Gets or sets the name of the film. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the rating of the film. + /// + public int Rating { get; set; } + + /// + /// Gets or sets the description of the film. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the list of genres associated with the film. + /// + public List Genres { get; set; } = new(); + + /// + /// Gets or sets the URL of the film's poster. + /// + public string? PosterUrl { get; set; } + + /// + /// Gets or sets the timestamp when the film was created. + /// + public DateTime Created { get; set; } + + /// + /// Gets or sets the unique identifier for the film. + /// + public int Id { get; set; } +} \ No newline at end of file diff --git a/Films/Films.Domain/Models/Genre.cs b/Films/Films.Domain/Models/Genre.cs new file mode 100644 index 0000000..7d16765 --- /dev/null +++ b/Films/Films.Domain/Models/Genre.cs @@ -0,0 +1,36 @@ +using Films.Domain.Models; +using Kirel.Repositories.Core.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace Films.Domain.Models; + +/// +/// Represents a genre associated with films. +/// +/*[Index(nameof(Name), IsUnique = true)]*/ +public class Genre : IKeyEntity, ICreatedAtTrackedEntity +{ + /// + /// Gets or sets the name of the genre. + /// + + public string? Name { get; set; } + + /// + /// reference to film + /// + public List? Film { get; set; } = new(); + + /// + /// Gets or sets the Created time of genre + /// + public DateTime Created { get; set; } + + /// + /// Gets or sets the unique identifier for the genre. + /// + public int Id { get; set; } + + + +} \ No newline at end of file diff --git a/Films/Films.Domain/Models/PaginatedResult.cs b/Films/Films.Domain/Models/PaginatedResult.cs new file mode 100644 index 0000000..47704bb --- /dev/null +++ b/Films/Films.Domain/Models/PaginatedResult.cs @@ -0,0 +1,102 @@ +namespace Films.Domain.Models; + +/// +/// Pagination class +/// +public class Pagination +{ + /// + /// Pagination constructor + /// + /// Total number of pages + /// Total number of items + /// Current page number + /// Size of the page + public Pagination(int totalPages = 0, int totalCount = 0, int currentPage = 1, int pageSize = 10) + { + TotalPages = totalPages; + TotalCount = totalCount; + CurrentPage = currentPage; + PageSize = pageSize; + } + + /// + /// Total number of pages + /// + public int TotalPages { get; set; } + + /// + /// Total number of items + /// + public int TotalCount { get; set; } + + /// + /// Current page number + /// + public int CurrentPage { get; set; } + + /// + /// Size of the page + /// + public int PageSize { get; set; } + + /// + /// Generates pagination of entities + /// + /// Page number + /// Page size + /// Total count + /// Pagination + public static Pagination Generate(int pageNumber = 0, int pageSize = 0, int totalCount = 0) + { + var page = pageNumber > 0 ? pageNumber : 1; + var size = pageSize > 0 ? pageSize : 10; + var pagination = new Pagination + { + CurrentPage = page, + PageSize = size, + TotalCount = totalCount, + TotalPages = (int)Math.Ceiling(totalCount / (double)size) + }; + return pagination; + } +} + +/// +/// Class pagination results +/// +/// Entity type +public class PaginatedResult +{ + /// + /// PaginatedResult constructor + /// + public PaginatedResult() + { + Pagination = new Pagination(); + } + + /// + /// PaginatedResult constructor + /// + /// A data with a specific type + /// Current page number + /// Size of the page + /// Total number of pages + /// Total number of items + public PaginatedResult(T data, int currentPage, int pageSize, int totalPages, int totalCount) + { + Pagination = new Pagination(totalPages, totalCount, currentPage, pageSize); + Data = data; + } + + /// + /// Pagination entity field + /// + public Pagination Pagination { get; set; } + + /// + /// A data field with a specific type + /// + public T? Data { get; set; } +} \ No newline at end of file diff --git a/Films/Films.Infrastructure/DbContext/FilmDbContext.cs b/Films/Films.Infrastructure/DbContext/FilmDbContext.cs new file mode 100644 index 0000000..a6779fb --- /dev/null +++ b/Films/Films.Infrastructure/DbContext/FilmDbContext.cs @@ -0,0 +1,40 @@ +using Films.Domain.Models; +using Microsoft.EntityFrameworkCore; + +namespace Films.Infrastructure; + +/// +/// Represents the database context for managing film-related data. +/// +public class FilmDbContext : DbContext +{ + /// + /// Initializes a new instance of the class. + /// + /// The options used to configure the context. + public FilmDbContext(DbContextOptions options) : base(options) + { + } + + + + /// + /// + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasMany(f => f.Genres) + .WithMany(g => g.Film) + .UsingEntity(j => j.ToTable("FilmGenres")); + } + /// + /// Gets or sets the DbSet for films in the database. + /// + private DbSet? Films { get; set; } + + /// + /// Gets or sets the DbSet for genres in the database. + /// + private DbSet? Genres { get; set; } +} \ No newline at end of file diff --git a/Films/Films.Infrastructure/DbContext/FilmDbInitialize.cs b/Films/Films.Infrastructure/DbContext/FilmDbInitialize.cs new file mode 100644 index 0000000..66f5ac2 --- /dev/null +++ b/Films/Films.Infrastructure/DbContext/FilmDbInitialize.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Films.Infrastructure; + +/// +/// Utility class for initializing the film database and creating tables. +/// +public class FilmDbInitialize : DbContext +{ + /// + /// Initializes the film database and creates tables if they do not exist. + /// + /// The service provider to retrieve the database context. + public static void Initialize(IServiceProvider serviceProvider) + { + var context = serviceProvider.GetRequiredService(); + context.Database.EnsureCreated(); + } +} \ No newline at end of file diff --git a/Films/Films.Infrastructure/Extentions/FilmsRepositoriesExtension.cs b/Films/Films.Infrastructure/Extentions/FilmsRepositoriesExtension.cs new file mode 100644 index 0000000..162dfa9 --- /dev/null +++ b/Films/Films.Infrastructure/Extentions/FilmsRepositoriesExtension.cs @@ -0,0 +1,25 @@ +using Films.Domain.Models; +using Kirel.Repositories.Core.Interfaces; +using Kirel.Repositories.EntityFramework; +using Microsoft.Extensions.DependencyInjection; + +namespace Films.Infrastructure.Extentions; + +/// +/// Add db to DI +/// +public static class FilmsRepositoriesExtension +{ + /// + /// + /// Collection services + public static void AddFilmsRepositories(this IServiceCollection services) + { + services + .AddScoped, + KirelGenericEntityFrameworkRepository>(); + services + .AddScoped, + KirelGenericEntityFrameworkRepository>(); + } +} \ No newline at end of file diff --git a/Films/Films.Infrastructure/Films.Infrastructure.csproj b/Films/Films.Infrastructure/Films.Infrastructure.csproj new file mode 100644 index 0000000..798427a --- /dev/null +++ b/Films/Films.Infrastructure/Films.Infrastructure.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + enable + true + true + + + + + + + + + + + + + + + diff --git a/Films/Films.sln b/Films/Films.sln new file mode 100644 index 0000000..fbe3187 --- /dev/null +++ b/Films/Films.sln @@ -0,0 +1,54 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Films.API", "Films.API\Films.API.csproj", "{8C91B590-7FD7-46AA-A34A-6A4D09A0BC9C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Films.Infrastructure", "Films.Infrastructure\Films.Infrastructure.csproj", "{7B56086C-9FA8-4B4E-A2B0-B2DC27990C34}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Films.Core", "Films.Core\Films.Core.csproj", "{3DCC9533-8898-4241-B8EC-504F0DBD0B2F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Films.Domain", "Films.Domain\Films.Domain.csproj", "{904C56F6-4160-4A1B-BA15-002D574FAE1F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Films.DTOs", "Films.DTOs\Films.DTOs.csproj", "{5B5FACA7-DABE-472C-8813-85A8C18A6A4D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Authentication.Shared", "Authentication.Shared\Authentication.Shared.csproj", "{C145BC0A-17A6-4CB3-A524-8355B41DD2CC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FilmsTest", "FilmsTest\FilmsTest.csproj", "{DB1F0DF7-8783-4B35-8DC8-184E4DA5A15F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8C91B590-7FD7-46AA-A34A-6A4D09A0BC9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C91B590-7FD7-46AA-A34A-6A4D09A0BC9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C91B590-7FD7-46AA-A34A-6A4D09A0BC9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C91B590-7FD7-46AA-A34A-6A4D09A0BC9C}.Release|Any CPU.Build.0 = Release|Any CPU + {7B56086C-9FA8-4B4E-A2B0-B2DC27990C34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B56086C-9FA8-4B4E-A2B0-B2DC27990C34}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B56086C-9FA8-4B4E-A2B0-B2DC27990C34}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B56086C-9FA8-4B4E-A2B0-B2DC27990C34}.Release|Any CPU.Build.0 = Release|Any CPU + {3DCC9533-8898-4241-B8EC-504F0DBD0B2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3DCC9533-8898-4241-B8EC-504F0DBD0B2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DCC9533-8898-4241-B8EC-504F0DBD0B2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3DCC9533-8898-4241-B8EC-504F0DBD0B2F}.Release|Any CPU.Build.0 = Release|Any CPU + {904C56F6-4160-4A1B-BA15-002D574FAE1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {904C56F6-4160-4A1B-BA15-002D574FAE1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {904C56F6-4160-4A1B-BA15-002D574FAE1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {904C56F6-4160-4A1B-BA15-002D574FAE1F}.Release|Any CPU.Build.0 = Release|Any CPU + {5B5FACA7-DABE-472C-8813-85A8C18A6A4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B5FACA7-DABE-472C-8813-85A8C18A6A4D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B5FACA7-DABE-472C-8813-85A8C18A6A4D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B5FACA7-DABE-472C-8813-85A8C18A6A4D}.Release|Any CPU.Build.0 = Release|Any CPU + {C145BC0A-17A6-4CB3-A524-8355B41DD2CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C145BC0A-17A6-4CB3-A524-8355B41DD2CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C145BC0A-17A6-4CB3-A524-8355B41DD2CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C145BC0A-17A6-4CB3-A524-8355B41DD2CC}.Release|Any CPU.Build.0 = Release|Any CPU + {DB1F0DF7-8783-4B35-8DC8-184E4DA5A15F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB1F0DF7-8783-4B35-8DC8-184E4DA5A15F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB1F0DF7-8783-4B35-8DC8-184E4DA5A15F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB1F0DF7-8783-4B35-8DC8-184E4DA5A15F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + EndGlobalSection +EndGlobal diff --git a/Films/FilmsTest/FilmsTest.csproj b/Films/FilmsTest/FilmsTest.csproj new file mode 100644 index 0000000..a492f73 --- /dev/null +++ b/Films/FilmsTest/FilmsTest.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + + + + + diff --git a/Films/FilmsTest/FilmsUnitTest.cs b/Films/FilmsTest/FilmsUnitTest.cs new file mode 100644 index 0000000..1849247 --- /dev/null +++ b/Films/FilmsTest/FilmsUnitTest.cs @@ -0,0 +1,212 @@ +using Films.Core.Mappers; +using Kirel.Repositories.Core.Models; +using Assert = NUnit.Framework.Assert; + +namespace FilmsTest +{ + [TestClass] + public class FilmServiceTests + { + private IMapper _mapper; + + [TestInitialize] + public void Initialize() + { + var configuration = new MapperConfiguration(cfg => + { + cfg.AddProfile(); + cfg.AddProfile(); // Регистрация вашего профиля маппинга + // Добавьте другие профили маппинга, если они есть + }); + + _mapper = configuration.CreateMapper(); + } + + [TestMethod] + public async Task CreateFilm_ValidModel_CallsRepositoriesAndService() + { + // Arrange + var mockFilmRepository = new Mock>(); + var mockGenreRepository = new Mock>(); + + var filmService = new FilmService( + mockFilmRepository.Object, + _mapper, // Используем зарегистрированный маппер + mockGenreRepository.Object + ); + + var filmCreateDto = new FilmCreateDto + { + Name = "Test Film", + Rating = 5, + Description = "Test Description", + Genres = new List + { + new() { Name = "Action" }, + new() { Name = "Drama" } + }, + PosterUrl = "http://example.com/test-poster.jpg" + }; + + // Assume that genre "Action" and "Drama" do not exist yet + mockGenreRepository.Setup(repo => repo.GetList( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new List()); + + // Act + await filmService.CreateFilm(filmCreateDto); + + // Assert + mockGenreRepository.Verify(m => m.Insert(It.IsAny()), + Times.Exactly(2)); // Verify that Insert is called for each new genre + mockFilmRepository.Verify(m => m.Insert(It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task SearchFilms_FoundFilms_ReturnsFilmDtos() + { + // Arrange + var mockFilmRepository = new Mock>(); + var mockGenreRepository = new Mock>(); + var mockMapper = new Mock(); + + var filmService = new FilmService( + mockFilmRepository.Object, + _mapper, // Используем зарегистрированный маппер + mockGenreRepository.Object + ); + + // Создаем поддельные данные для поиска + var filmName = "Test Film"; + var existingFilms = new List + { + new Film { Name = "Test Film 1" }, + new Film { Name = "Test Film 2" } + }; + + // Ожидаем, что метод GetList будет вызван с правильными параметрами + mockFilmRepository.Setup(repo => repo.GetList( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(existingFilms); + + // Ожидаем, что маппер будет вызван для маппинга Film в FilmDto + mockMapper.Setup(m => m.Map>(existingFilms)) + .Returns(new List + { + new FilmDto { Name = "Test Film 1" }, + new FilmDto { Name = "Test Film 2" } + }); + + // Act + var result = await filmService.SearchFilms(filmName); + + // Output результатов в консоль + Console.WriteLine("Search Films Result:"); + foreach (var filmDto in result) + { + Console.WriteLine($"Film Name: {filmDto.Name}"); + // Другие поля + } + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(2, result.Count); // Проверяем, что количество Dto соответствует ожидаемому + // Другие проверки по вашим ожиданиям могут быть добавлены здесь + } + + [TestMethod] + public async Task GetAllFilmsPaginated_ReturnsPaginatedResultWithGenres() + { + // Arrange + var mockFilmRepository = new Mock>(); + var mockGenreRepository = new Mock>(); + + var filmService = new FilmService( + mockFilmRepository.Object, + _mapper, // Используем зарегистрированный маппер + mockGenreRepository.Object + ); + + // Создаем поддельные данные для пагинации + var pageNumber = 1; + var pageSize = 10; + var orderBy = "Name"; + var orderDirection = SortDirection.Asc; + var search = "Test"; + + var totalCount = 20; // Общее количество фильмов + + + + var paginatedFilms = new List + { + new Film + { + Name = "Test Film 1", + Genres = new List + { + new Genre { Name = "Action" }, + new Genre { Name = "Drama" } + } + }, + new Film + { + Name = "Test Film 2", + Genres = new List + { + new Genre { Name = "Comedy" } + } + } + // Другие фильмы + }; + + // Ожидаем, что метод Count будет вызван с правильными параметрами и вернет общее количество фильмов + mockFilmRepository.Setup(repo => repo.Count(search)) + .ReturnsAsync(totalCount); + + // Ожидаем, что метод GetList будет вызван с правильными параметрами и вернет пагинированный список фильмов + mockFilmRepository.Setup(repo => repo.GetList( + It.IsAny(), + orderBy, + orderDirection, + pageNumber, + pageSize)) + .ReturnsAsync(paginatedFilms); + + // Act + var result = await filmService.GetAllFilmsPaginated(pageNumber, pageSize, orderBy, orderDirection, search); + Console.WriteLine("Pagination:"); + Console.WriteLine($" Current Page: {result.Pagination.CurrentPage}"); + Console.WriteLine($" Page Size: {result.Pagination.PageSize}"); + Console.WriteLine($" Total Count: {result.Pagination.TotalCount}"); + + foreach (var filmDto in result.Data) + { + Console.WriteLine("Film:"); + Console.WriteLine($" Name: {filmDto.Name}"); + Console.WriteLine($" Genres: {string.Join(", ", filmDto.Genres)}"); + // Другие поля + } + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(pageNumber, result.Pagination.CurrentPage); + Assert.AreEqual(pageSize, result.Pagination.PageSize); + Assert.AreEqual(totalCount, result.Pagination.TotalCount); + Assert.AreEqual(paginatedFilms.Count, result.Data.Count); + + // Проверяем, что жанры также корректно маппируются + Assert.AreEqual(paginatedFilms[0].Genres.Count, result.Data[0].Genres.Count); + Assert.AreEqual(paginatedFilms[1].Genres.Count, result.Data[1].Genres.Count); + // Другие проверки по вашим ожиданиям могут быть добавлены здесь + } + } +} diff --git a/Films/FilmsTest/Usings.cs b/Films/FilmsTest/Usings.cs new file mode 100644 index 0000000..40fd8ea --- /dev/null +++ b/Films/FilmsTest/Usings.cs @@ -0,0 +1,9 @@ +global using NUnit.Framework; +global using System.Linq.Expressions; +global using AutoMapper; +global using Films.Core.Services; +global using Films.Domain.Models; +global using Films.DTOs; +global using Kirel.Repositories.Core.Interfaces; +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Moq; diff --git a/Films/global.json b/Films/global.json new file mode 100644 index 0000000..1bcf6c0 --- /dev/null +++ b/Films/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "6.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file