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