diff --git a/.github/README.md b/.github/README.md
new file mode 100644
index 000000000..e11125a3a
--- /dev/null
+++ b/.github/README.md
@@ -0,0 +1,131 @@
+# GitHub Actions Workflows
+
+Данный репозиторий содержит набор GitHub Actions workflows для автоматизации CI/CD процессов для ASP.NET Core проекта клиники.
+
+## Доступные Workflows
+
+### 1. CI Pipeline (`ci.yml`)
+**Триггеры:** Push в `main`/`develop`, Pull Request в `main`/`develop`
+
+**Функции:**
+- Сборка .NET 9.0 проектов
+- Восстановление NuGet зависимостей
+- Запуск тестов (если есть)
+- Кэширование NuGet пакетов для ускорения сборки
+
+**Использование:**
+```bash
+# Локальная сборка
+dotnet restore Clinic/Clinic.sln
+dotnet build Clinic/Clinic.sln --configuration Release
+dotnet test Clinic/Clinic.sln --configuration Release
+```
+
+### 2. Docker Build and Deploy (`docker.yml`)
+**Триггеры:** Push в `main`, теги `v*`, Pull Request в `main`
+
+**Функции:**
+- Создание Docker образа для API
+- Публикация образа в GitHub Container Registry
+- Поддержка версионирования образов
+
+**Использование:**
+```bash
+# Сборка Docker образа
+docker build -f Clinic/Clinic.Api/Dockerfile -t clinic-api:latest .
+
+# Запуск контейнера
+docker run -p 8080:8080 clinic-api:latest
+```
+
+### 3. Azure Deploy (`azure-deploy.yml`)
+**Триггеры:** Push в `main`, ручной запуск
+
+**Функции:**
+- Публикация .NET проекта
+- Деплой в Azure App Service
+
+**Необходимые Secrets:**
+- `AZURE_WEBAPP_NAME` - имя Azure App Service
+- `AZURE_WEBAPP_PUBLISH_PROFILE` - профиль публикации
+
+**Настройка Azure:**
+1. Создайте Azure App Service
+2. Скачайте профиль публикации
+3. Добавьте Secrets в GitHub репозиторий
+
+## Структура файлов
+
+```
+.github/
+├── workflows/
+│ ├── ci.yml # Основной CI pipeline
+│ ├── docker.yml # Docker сборка и деплой
+│ ├── azure-deploy.yml # Azure App Service деплой
+│ └── setup_pr.yml # Автоматическая настройка PR
+├── ISSUE_TEMPLATE/
+│ └── вопрос-по-лабораторной.md
+├── PULL_REQUEST_TEMPLATE.md
+└── DISCUSSION_TEMPLATE/
+ └── questions.yml
+
+Clinic/
+├── Clinic.Api/
+│ ├── Dockerfile # Docker образ для API
+│ └── .dockerignore # Исключения для Docker сборки
+└── ...
+```
+
+## Настройка проекта
+
+### Для локальной разработки:
+```bash
+# Восстановление зависимостей
+dotnet restore Clinic/Clinic.sln
+
+# Сборка проекта
+dotnet build Clinic/Clinic.sln
+
+# Запуск API
+dotnet run --project Clinic/Clinic.Api
+
+# Запуск с настройками разработки
+cd Clinic/Clinic.Api
+dotnet run
+```
+
+### Для Docker:
+```bash
+# Сборка образа
+docker build -f Clinic/Clinic.Api/Dockerfile -t clinic-api .
+
+# Запуск с базой данных (пример)
+docker run -p 8080:8080 -e ConnectionStrings__DefaultConnection="Server=host.docker.internal;Database=ClinicDb;User=sa;Password=YourPassword;" clinic-api
+```
+
+## Переменные окружения
+
+Для работы проекта необходимо настроить следующие переменные окружения:
+
+### Development:
+- `ASPNETCORE_ENVIRONMENT=Development`
+- `ConnectionStrings__DefaultConnection` - строка подключения к БД
+
+### Production:
+- `ASPNETCORE_ENVIRONMENT=Production`
+- `ConnectionStrings__DefaultConnection` - строка подключения к продакшн БД
+
+## Дополнительные возможности
+
+### Добавление тестов:
+1. Создайте тестовый проект: `dotnet new xunit -n Clinic.Tests`
+2. Добавьте ссылку на основной проект
+3. CI pipeline автоматически запустит тесты
+
+### Мониторинг:
+- Логи Azure App Service доступны через Azure Portal
+- Docker контейнеры можно мониторить через Docker Desktop или Azure Container Instances
+
+### Безопасность:
+- Используйте GitHub Secrets для хранения чувствительных данных
+- Не коммитьте ключи подключения к БД в репозиторий
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..8b3f0f53c
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,30 @@
+name: Build Check
+
+on:
+ push:
+ branches: [ main, develop ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Restore dependencies
+ run: dotnet restore Clinic/Clinic.sln
+
+ - name: Build solution
+ run: dotnet build Clinic/Clinic.sln --configuration Release --no-restore
+
+ - name: Test solution
+ run: dotnet test ./Clinic/Clinic.Tests/Clinic.Tests.csproj --configuration Release --no-build --verbosity normal
+
diff --git a/Clinic/Clinic.Api/Clinic.Api.csproj b/Clinic/Clinic.Api/Clinic.Api.csproj
new file mode 100644
index 000000000..87340686c
--- /dev/null
+++ b/Clinic/Clinic.Api/Clinic.Api.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+ $(NoWarn);1591
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Clinic/Clinic.Api/Controllers/AnalyticsControllers.cs b/Clinic/Clinic.Api/Controllers/AnalyticsControllers.cs
new file mode 100644
index 000000000..76c1d928a
--- /dev/null
+++ b/Clinic/Clinic.Api/Controllers/AnalyticsControllers.cs
@@ -0,0 +1,76 @@
+using Microsoft.AspNetCore.Mvc;
+using Clinic.Application.Services;
+using Clinic.Application.DTOs.Doctor;
+using Clinic.Application.DTOs.Patient;
+using Clinic.Application.DTOs.Appointment;
+
+namespace Clinic.Api.Controllers;
+
+///
+/// Controller for analytics and business logic queries about doctors, patients, and appointments.
+///
+[ApiController]
+[Route("api/analytics")]
+public class AnalyticsController(AnalyticsServices testServices) : ControllerBase
+{
+ ///
+ /// Retrieves a list of doctors with 10 or more years of experience.
+ /// Returns a 200 OK with a list of GetDoctorDto objects.
+ ///
+ [HttpGet("doctors/experience")]
+ [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)]
+ public ActionResult> GetDoctorsWithExperience()
+ {
+ return Ok(testServices.GetDoctorsWithExperience10YearsOrMore());
+ }
+
+ ///
+ /// Fetches patients assigned to a specific doctor (by doctorId), ordered by their full name.
+ /// Returns 200 OK with a list of GetPatientDto objects if the doctor exists; otherwise, 404 Not Found with a message.
+ ///
+ [HttpGet("patients/doctor")]
+ [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult> GetPatientsByDoctor(int doctorId)
+ {
+ var result = testServices.GetPatientsByDoctorOrderedByFullName(doctorId);
+ if (result == null)
+ {
+ return NotFound("Doctor not found.");
+ }
+ return Ok(result);
+ }
+
+ ///
+ /// Returns the count of return visits (as an integer) that occurred in the last month.
+ /// Responds with 200 OK.
+ ///
+ [HttpGet("patients/return-visits")]
+ [ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
+ public ActionResult GetReturnVisitsCountLastMonth()
+ {
+ return Ok(testServices.GetReturnVisitsCountLastMonth());
+ }
+
+ ///
+ /// Gets a list of patients over 30 years old who are associated with multiple doctors, ordered by birth date.
+ /// Returns 200 OK with a list of GetPatientDto objects.
+ ///
+ [HttpGet("patients/over-30")]
+ [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)]
+ public ActionResult> GetPatientsOver30WithMultipleDoctors()
+ {
+ return Ok(testServices.GetPatientsOver30WithMultipleDoctorsOrderedByBirthDate());
+ }
+
+ ///
+ /// Retrieves appointments scheduled in a specific room (roomNumber) for the current month.
+ /// Returns 200 OK with a list of GetAppointmentDto objects.
+ ///
+ [HttpGet("appointments/room")]
+ [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)]
+ public ActionResult> GetAppointmentsInRoomForCurrentMonth(int roomNumber)
+ {
+ return Ok(testServices.GetAppointmentsInRoomForCurrentMonth(roomNumber));
+ }
+}
\ No newline at end of file
diff --git a/Clinic/Clinic.Api/Controllers/AppointmentControllers.cs b/Clinic/Clinic.Api/Controllers/AppointmentControllers.cs
new file mode 100644
index 000000000..65512ce50
--- /dev/null
+++ b/Clinic/Clinic.Api/Controllers/AppointmentControllers.cs
@@ -0,0 +1,50 @@
+using Microsoft.AspNetCore.Mvc;
+using Clinic.Application.Interfaces.Services;
+using Clinic.Application.DTOs.Appointment;
+
+namespace Clinic.Api.Controllers;
+
+///
+/// Controller for handling appointment-related operations such as retrieving, creating, updating,
+/// and deleting appointments in the clinic.
+///
+[ApiController]
+[Route("api/appointments")]
+public class AppointmentControllers(IAppointmentServices appointmentServices) : BaseControllers(appointmentServices)
+{
+ ///
+ /// Gets all appointments for a specific doctor.
+ ///
+ /// The doctor's id.
+ /// ActionResult containing appointments or NotFound if doctor not found.
+ [HttpGet("doctor/{doctorId}")]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult> GetByDoctor(int doctorId)
+ {
+ var appointments = appointmentServices.GetAppointmentsByDoctor(doctorId);
+ if (appointments == null)
+ {
+ return NotFound("Doctor not found.");
+ }
+ return Ok(appointments);
+ }
+
+ ///
+ /// Gets all appointments for a specific patient.
+ ///
+ /// The patient's id.
+ /// ActionResult containing appointments or NotFound if patient not found.
+ [HttpGet("patient/{patientId}")]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult> GetByPatient(int patientId)
+ {
+ var appointments = appointmentServices.GetAppointmentsByPatient(patientId);
+ if (appointments == null)
+ {
+ return NotFound("Patient not found.");
+ }
+ return Ok(appointments);
+ }
+}
diff --git a/Clinic/Clinic.Api/Controllers/BaseControllers.cs b/Clinic/Clinic.Api/Controllers/BaseControllers.cs
new file mode 100644
index 000000000..739f20baa
--- /dev/null
+++ b/Clinic/Clinic.Api/Controllers/BaseControllers.cs
@@ -0,0 +1,126 @@
+using Microsoft.AspNetCore.Mvc;
+using Clinic.Application.Interfaces.Services;
+
+namespace Clinic.Api.Controllers;
+
+///
+/// Base controller class that provides common CRUD operations for all controllers.
+///
+/// DTO type for retrieving entity data.
+/// DTO type for create and update operations.
+public class BaseControllers(IBaseServices Service) : ControllerBase
+ where TGetDto : class
+ where TSaveDto : class
+{
+
+ ///
+ /// Gets all entities.
+ ///
+ /// ActionResult containing a list of all entities.
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public virtual ActionResult> GetAll()
+ {
+ var entities = Service.GetAll();
+ return Ok(entities);
+ }
+
+ ///
+ /// Gets a specific entity by its id.
+ ///
+ /// The id of the entity.
+ /// ActionResult containing the entity or NotFound if not found.
+ [HttpGet("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public virtual ActionResult Get(int id)
+ {
+ var entity = Service.Get(id);
+ if (entity == null)
+ {
+ return NotFound(GetEntityName() + " not found.");
+ }
+ return Ok(entity);
+ }
+
+ ///
+ /// Creates a new entity.
+ ///
+ /// The creation data.
+ /// ActionResult containing the created entity or BadRequest if creation fails.
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public virtual ActionResult Create(TSaveDto dto)
+ {
+ var entity = Service.Create(dto);
+ if (entity == null)
+ {
+ return BadRequest("Could not create " + GetEntityName() + ".");
+ }
+ var id = GetEntityId(entity);
+ return CreatedAtAction(nameof(Get), new { id }, entity);
+ }
+
+ ///
+ /// Updates an existing entity.
+ ///
+ /// The id of the entity to update.
+ /// The update data.
+ /// ActionResult containing the updated entity or NotFound if not found.
+ [HttpPut("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public virtual ActionResult Update(int id, TSaveDto dto)
+ {
+ var entity = Service.Update(id, dto);
+ if (entity == null)
+ {
+ return NotFound(GetEntityName() + " not found.");
+ }
+ return Ok(entity);
+ }
+
+ ///
+ /// Deletes an entity by its id.
+ ///
+ /// The id of the entity to delete.
+ /// ActionResult indicating success or NotFound if not found.
+ [HttpDelete("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public virtual ActionResult Delete(int id)
+ {
+ var result = Service.Delete(id);
+ if (!result)
+ {
+ return NotFound(GetEntityName() + " not found.");
+ }
+ return Ok(GetEntityName() + " deleted successfully.");
+ }
+
+ ///
+ /// Gets the entity name for error messages.
+ ///
+ /// The entity name.
+ protected virtual string GetEntityName()
+ {
+ var name = typeof(TGetDto).Name;
+ return name.Replace("Get", "").Replace("Dto", "");
+ }
+
+ ///
+ /// Gets the entity ID using reflection.
+ ///
+ /// The entity DTO.
+ /// The entity ID.
+ protected virtual int GetEntityId(TGetDto entity)
+ {
+ var idProperty = typeof(TGetDto).GetProperty("Id");
+ if (idProperty != null && idProperty.GetValue(entity) is int id)
+ {
+ return id;
+ }
+ return 0;
+ }
+}
diff --git a/Clinic/Clinic.Api/Controllers/DoctorControllers.cs b/Clinic/Clinic.Api/Controllers/DoctorControllers.cs
new file mode 100644
index 000000000..51a0d018a
--- /dev/null
+++ b/Clinic/Clinic.Api/Controllers/DoctorControllers.cs
@@ -0,0 +1,13 @@
+using Microsoft.AspNetCore.Mvc;
+using Clinic.Application.DTOs.Doctor;
+using Clinic.Application.Interfaces.Services;
+
+namespace Clinic.Api.Controllers;
+
+///
+/// Controller for handling doctor-related operations such as retrieving,
+/// creating, updating, and deleting doctors in the clinic.
+///
+[ApiController]
+[Route("api/doctors")]
+public class DoctorControllers(IDoctorServices doctorServices) : BaseControllers(doctorServices);
diff --git a/Clinic/Clinic.Api/Controllers/PatientControllers.cs b/Clinic/Clinic.Api/Controllers/PatientControllers.cs
new file mode 100644
index 000000000..75180d8f8
--- /dev/null
+++ b/Clinic/Clinic.Api/Controllers/PatientControllers.cs
@@ -0,0 +1,13 @@
+using Microsoft.AspNetCore.Mvc;
+using Clinic.Application.DTOs.Patient;
+using Clinic.Application.Interfaces.Services;
+
+namespace Clinic.Api.Controllers;
+
+///
+/// Controller for managing patient operations such as retrieving,
+/// creating, updating, and deleting patient records.
+///
+[ApiController]
+[Route("api/patients")]
+public class PatientControllers(IPatientServices patientServices) : BaseControllers(patientServices);
diff --git a/Clinic/Clinic.Api/Controllers/SpecializationControllers.cs b/Clinic/Clinic.Api/Controllers/SpecializationControllers.cs
new file mode 100644
index 000000000..7909a33cb
--- /dev/null
+++ b/Clinic/Clinic.Api/Controllers/SpecializationControllers.cs
@@ -0,0 +1,13 @@
+using Microsoft.AspNetCore.Mvc;
+using Clinic.Application.DTOs.Specialization;
+using Clinic.Application.Interfaces.Services;
+
+namespace Clinic.Api.Controllers;
+
+///
+/// Controller for managing specialization operations such as retrieving,
+/// creating, and deleting specializations.
+///
+[ApiController]
+[Route("api/specializations")]
+public class SpecializationControllers(ISpecializationServices specializationServices) : BaseControllers(specializationServices);
diff --git a/Clinic/Clinic.Api/Converter/DataConverter.cs b/Clinic/Clinic.Api/Converter/DataConverter.cs
new file mode 100644
index 000000000..ac63b6a78
--- /dev/null
+++ b/Clinic/Clinic.Api/Converter/DataConverter.cs
@@ -0,0 +1,31 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Clinic.Api.Converter;
+
+///
+/// Custom JSON converter for DateOnly type, handling serialization and deserialization using the "yyyy-MM-dd" format.
+///
+public class DateConverter : JsonConverter
+{
+ private const string Format = "yyyy-MM-dd";
+
+ ///
+ /// Deserializes a JSON string in "yyyy-MM-dd" format to a DateOnly object.
+ ///
+ /// The UTF-8 JSON reader.
+ /// The type to convert (DateOnly).
+ /// The serializer options.
+ /// The parsed DateOnly value.
+ public override DateOnly Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
+ => DateOnly.ParseExact(reader.GetString()!, Format);
+
+ ///
+ /// Serializes a DateOnly object to a JSON string in "yyyy-MM-dd" format.
+ ///
+ /// The UTF-8 JSON writer.
+ /// The DateOnly value to serialize.
+ /// The serializer options.
+ public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
+ => writer.WriteStringValue(value.ToString(Format));
+}
\ No newline at end of file
diff --git a/Clinic/Clinic.Api/Grpc/ContractIngestService.cs b/Clinic/Clinic.Api/Grpc/ContractIngestService.cs
new file mode 100644
index 000000000..13a875a8a
--- /dev/null
+++ b/Clinic/Clinic.Api/Grpc/ContractIngestService.cs
@@ -0,0 +1,140 @@
+using System.Globalization;
+using Clinic.Application.Ports;
+using Clinic.Contracts;
+using Clinic.Models.Entities;
+using Grpc.Core;
+
+namespace Clinic.Api.Grpc;
+
+///
+/// gRPC service that ingests appointment contracts from a client stream
+/// and persists mapped appointments to storage.
+///
+public class ContractIngestService : ContractIngestor.ContractIngestorBase
+{
+ private readonly IAppointmentRepository _appointments;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Appointment repository used to persist mapped contracts.
+ /// Logger for ingest diagnostics and persistence errors.
+ public ContractIngestService(IAppointmentRepository appointments, ILogger logger)
+ {
+ _appointments = appointments;
+ _logger = logger;
+ }
+
+ ///
+ /// Reads contracts from the incoming gRPC stream, validates and maps them to appointments,
+ /// and returns aggregated ingest statistics.
+ ///
+ /// Incoming stream of contracts.
+ /// Server call context for cancellation and metadata.
+ /// Ingest result with total received, saved, failed and error details.
+ public override async Task IngestContracts(IAsyncStreamReader requestStream, ServerCallContext context)
+ {
+ var received = 0;
+ var saved = 0;
+ var failed = 0;
+ var errors = new List();
+
+ await foreach (var contract in requestStream.ReadAllAsync(context.CancellationToken))
+ {
+ received++;
+ if (!TryMap(contract, out var appointment, out var error))
+ {
+ failed++;
+ errors.Add($"#{received}: {error}");
+ continue;
+ }
+
+ try
+ {
+ if (_appointments.AddAppointment(appointment))
+ {
+ saved++;
+ }
+ else
+ {
+ failed++;
+ errors.Add($"#{received}: Appointment already exists.");
+ }
+ }
+ catch (Exception ex)
+ {
+ failed++;
+ errors.Add($"#{received}: {ex.Message}");
+ _logger.LogWarning(ex, "Failed to persist contract #{Index}", received);
+ }
+ }
+
+ var result = new IngestResult
+ {
+ Received = received,
+ Saved = saved,
+ Failed = failed
+ };
+ result.Errors.AddRange(errors);
+
+ return result;
+ }
+
+ ///
+ /// Validates contract payload and converts it to an .
+ ///
+ /// Source contract.
+ /// Mapped appointment if conversion succeeds.
+ /// Validation or conversion error message.
+ /// true when contract is valid and mapped; otherwise, false.
+ private static bool TryMap(Contract contract, out Appointment appointment, out string error)
+ {
+ appointment = null!;
+ error = string.Empty;
+
+ if (contract.PatientId <= 0)
+ {
+ error = "PatientId must be positive.";
+ return false;
+ }
+
+ if (contract.DoctorId <= 0)
+ {
+ error = "DoctorId must be positive.";
+ return false;
+ }
+
+ if (string.IsNullOrWhiteSpace(contract.PatientFullName))
+ {
+ error = "PatientFullName is required.";
+ return false;
+ }
+
+ if (string.IsNullOrWhiteSpace(contract.DoctorFullName))
+ {
+ error = "DoctorFullName is required.";
+ return false;
+ }
+
+ if (!DateTimeOffset.TryParse(contract.AppointmentTime, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dateTimeOffset))
+ {
+ error = "AppointmentTime must be ISO-8601.";
+ return false;
+ }
+
+ appointment = new Appointment
+ {
+ Id = 0,
+ PatientId = contract.PatientId,
+ PatientFullName = contract.PatientFullName,
+ DoctorId = contract.DoctorId,
+ DoctorFullName = contract.DoctorFullName,
+ DateTime = dateTimeOffset.UtcDateTime,
+ RoomNumber = contract.RoomNumber,
+ IsReturnVisit = contract.IsReturnVisit
+ };
+
+ return true;
+ }
+}
diff --git a/Clinic/Clinic.Api/Program.cs b/Clinic/Clinic.Api/Program.cs
new file mode 100644
index 000000000..74d7d7ddb
--- /dev/null
+++ b/Clinic/Clinic.Api/Program.cs
@@ -0,0 +1,70 @@
+using Clinic.DataBase;
+using Clinic.Application.Ports;
+using Clinic.DataBase.EntityFramework;
+using Clinic.Application.Services.Mapping;
+using Microsoft.Extensions.Hosting;
+using Clinic.Application.Services;
+using Clinic.Api.Converter;
+using Clinic.Application.Interfaces.Services;
+using Microsoft.EntityFrameworkCore;
+using Clinic.Api.Grpc;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddControllers()
+ .AddJsonOptions(options =>
+ {
+ options.JsonSerializerOptions.Converters.Add(new DateConverter());
+ options.JsonSerializerOptions.PropertyNamingPolicy = null;
+ });
+
+builder.Services.AddGrpc();
+
+var connectionString = builder.Configuration.GetConnectionString("ClinicDb")
+ ?? throw new InvalidOperationException("Connection string 'ClinicDb' is not configured.");
+
+builder.Services.AddDbContext(options =>
+ options.UseNpgsql(connectionString));
+
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddAutoMapper(cfg => cfg.AddProfile());
+
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(options =>
+{
+ var xmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml";
+ var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
+ options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true);
+});
+
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+
+var app = builder.Build();
+
+
+using (var scope = app.Services.CreateScope())
+{
+ var db = scope.ServiceProvider.GetRequiredService();
+ db.Database.Migrate();
+}
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI(options =>
+ {
+ options.SwaggerEndpoint("/swagger/v1/swagger.json", "Clinic API V1");
+ options.RoutePrefix = "swagger";
+ });
+}
+
+app.MapGrpcService();
+app.MapControllers();
+app.Run();
diff --git a/Clinic/Clinic.Api/Properties/launchSettings.json b/Clinic/Clinic.Api/Properties/launchSettings.json
new file mode 100644
index 000000000..13035bd70
--- /dev/null
+++ b/Clinic/Clinic.Api/Properties/launchSettings.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5042",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7015;http://localhost:5042",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/Clinic/Clinic.Api/appsettings.Development.json b/Clinic/Clinic.Api/appsettings.Development.json
new file mode 100644
index 000000000..3c7c27b74
--- /dev/null
+++ b/Clinic/Clinic.Api/appsettings.Development.json
@@ -0,0 +1,13 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Microsoft.EntityFrameworkCore": "Warning",
+ "Microsoft.EntityFrameworkCore.Database.Command": "Warning"
+ }
+ },
+ "ConnectionStrings": {
+ "ClinicDb": "Host=localhost;Database=ClinicDb;Username=clinic_app;Password=clinic123;Include Error Detail=true"
+ }
+}
diff --git a/Clinic/Clinic.Api/appsettings.json b/Clinic/Clinic.Api/appsettings.json
new file mode 100644
index 000000000..4e606cea4
--- /dev/null
+++ b/Clinic/Clinic.Api/appsettings.json
@@ -0,0 +1,13 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Microsoft.EntityFrameworkCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "ClinicDb": "Host=localhost;Database=ClinicDb;Username=clinic_app;Password=clinic123"
+ }
+}
diff --git a/Clinic/Clinic.AppHost/AppHost.cs b/Clinic/Clinic.AppHost/AppHost.cs
new file mode 100644
index 000000000..68e73bbb4
--- /dev/null
+++ b/Clinic/Clinic.AppHost/AppHost.cs
@@ -0,0 +1,26 @@
+if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DOTNET_DASHBOARD_OTLP_ENDPOINT_URL")) &&
+ string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL")))
+{
+ Environment.SetEnvironmentVariable("DOTNET_DASHBOARD_OTLP_ENDPOINT_URL", "http://127.0.0.1:4317");
+ Environment.SetEnvironmentVariable("DOTNET_DASHBOARD_OTLP_HTTP_ENDPOINT_URL", "http://127.0.0.1:4318");
+}
+
+if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("ASPIRE_ALLOW_UNSECURED_TRANSPORT")))
+{
+ Environment.SetEnvironmentVariable("ASPIRE_ALLOW_UNSECURED_TRANSPORT", "true");
+}
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+var postgresql = builder.AddPostgres("postgres")
+ .AddDatabase("ClinicDb");
+
+var api = builder.AddProject("clinic-api", "../Clinic.Api/Clinic.Api.csproj")
+ .WithReference(postgresql)
+ .WithExternalHttpEndpoints();
+
+var appointmentGenerator = builder.AddProject("clinic-appointment-generator", "../Clinic.AppointmentGenerator/Clinic.AppointmentGenerator.csproj")
+ .WithReference(postgresql)
+ .WithEnvironment("Grpc__Endpoint", api.GetEndpoint("https"));
+
+builder.Build().Run();
diff --git a/Clinic/Clinic.AppHost/Clinic.AppHost.csproj b/Clinic/Clinic.AppHost/Clinic.AppHost.csproj
new file mode 100644
index 000000000..40a19d8f4
--- /dev/null
+++ b/Clinic/Clinic.AppHost/Clinic.AppHost.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ 1fced4c2-e45b-4353-84db-0376db0b16fc
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Clinic/Clinic.AppHost/Properties/launchSettings.json b/Clinic/Clinic.AppHost/Properties/launchSettings.json
new file mode 100644
index 000000000..29cc2b1ee
--- /dev/null
+++ b/Clinic/Clinic.AppHost/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17260;http://localhost:15245",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21029",
+ "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23235",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22009"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15245",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19118",
+ "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18229",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20170"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Clinic/Clinic.AppHost/appsettings.Development.json b/Clinic/Clinic.AppHost/appsettings.Development.json
new file mode 100644
index 000000000..ff66ba6b2
--- /dev/null
+++ b/Clinic/Clinic.AppHost/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/Clinic/Clinic.AppHost/appsettings.json b/Clinic/Clinic.AppHost/appsettings.json
new file mode 100644
index 000000000..2185f9551
--- /dev/null
+++ b/Clinic/Clinic.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/Clinic/Clinic.Application.Services/Clinic.Application.Services.csproj b/Clinic/Clinic.Application.Services/Clinic.Application.Services.csproj
new file mode 100644
index 000000000..8f0424312
--- /dev/null
+++ b/Clinic/Clinic.Application.Services/Clinic.Application.Services.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Clinic/Clinic.Application.Services/Mapping/MappingProfile.cs b/Clinic/Clinic.Application.Services/Mapping/MappingProfile.cs
new file mode 100644
index 000000000..d19f3edee
--- /dev/null
+++ b/Clinic/Clinic.Application.Services/Mapping/MappingProfile.cs
@@ -0,0 +1,46 @@
+using AutoMapper;
+using Clinic.Application.DTOs.Patient;
+using Clinic.Application.DTOs.Doctor;
+using Clinic.Application.DTOs.Specialization;
+using Clinic.Application.DTOs.Appointment;
+using Clinic.Models.Enums;
+using Clinic.Models.Entities;
+
+namespace Clinic.Application.Services.Mapping;
+
+///
+/// AutoMapper profile for mapping between DTOs and entity models in the Clinic API.
+/// Defines the mapping rules for Patient, Doctor, Specialization, and Appointment objects,
+/// including custom enum conversions and member mapping settings.
+///
+public class MappingProfile : Profile
+{
+ public MappingProfile()
+ {
+ CreateMap()
+ .ForMember(dest => dest.Gender, opt => opt.MapFrom(src => Enum.Parse(src.Gender)))
+ .ForMember(dest => dest.BloodGroup, opt => opt.MapFrom(src => Enum.Parse(src.BloodGroup)))
+ .ForMember(dest => dest.RhesusFactor, opt => opt.MapFrom(src => Enum.Parse(src.RhesusFactor)));
+
+ CreateMap()
+ .ForMember(dest => dest.Gender, opt => opt.MapFrom(src => src.Gender.ToString()))
+ .ForMember(dest => dest.BloodGroup, opt => opt.MapFrom(src => src.BloodGroup.ToString()))
+ .ForMember(dest => dest.RhesusFactor, opt => opt.MapFrom(src => src.RhesusFactor.ToString()));
+
+ CreateMap()
+ .ForMember(dest => dest.Gender, opt => opt.MapFrom(src => Enum.Parse(src.Gender)))
+ .ForMember(dest => dest.Specializations, opt => opt.MapFrom(src => src.Specializations));
+
+ CreateMap()
+ .ForMember(dest => dest.Specializations, opt => opt.MapFrom(src => src.Specializations.Select(s => s.Name.ToString()).ToList()));
+
+ CreateMap()
+ .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name.ToString()));
+
+ CreateMap();
+
+ CreateMap();
+
+ CreateMap();
+ }
+}
diff --git a/Clinic/Clinic.Application.Services/Services/AnalyticsServices.cs b/Clinic/Clinic.Application.Services/Services/AnalyticsServices.cs
new file mode 100644
index 000000000..94712e0c0
--- /dev/null
+++ b/Clinic/Clinic.Application.Services/Services/AnalyticsServices.cs
@@ -0,0 +1,115 @@
+using AutoMapper;
+using Clinic.Application.Ports;
+using Clinic.Application.DTOs.Patient;
+using Clinic.Application.DTOs.Doctor;
+using Clinic.Application.DTOs.Appointment;
+
+namespace Clinic.Application.Services;
+
+public class AnalyticsServices
+{
+ private readonly IPatientRepository _patients;
+ private readonly IDoctorRepository _doctors;
+ private readonly IAppointmentRepository _appointments;
+ private readonly IMapper _mapper;
+
+ public AnalyticsServices(
+ IPatientRepository patients,
+ IDoctorRepository doctors,
+ IAppointmentRepository appointments,
+ IMapper mapper)
+ {
+ _patients = patients;
+ _doctors = doctors;
+ _appointments = appointments;
+ _mapper = mapper;
+ }
+
+ ///
+ /// Display information about all doctors with at least 10 years of experience.
+ ///
+ public IReadOnlyList GetDoctorsWithExperience10YearsOrMore(){
+ var doctors = _doctors.GetAllDoctors()
+ .Where(d => d.ExperienceYears >= 10)
+ .ToList();
+ return _mapper.Map>(doctors);
+ }
+
+ ///
+ /// Display information about all patients scheduled to see a specified doctor, sorted by full name.
+ ///
+ public IReadOnlyList? GetPatientsByDoctorOrderedByFullName(int doctorId){
+ var doctor = _doctors.GetDoctor(doctorId);
+ if (doctor == null)
+ {
+ return null;
+ }
+
+ var appointments = _appointments.GetAppointmentsByDoctor(doctorId);
+ var patients = appointments
+ .Where(a => a.DoctorId == doctorId)
+ .Select(a => _patients.GetPatient(a.PatientId))
+ .Distinct()
+ .ToList();
+
+ return _mapper.Map>(patients);
+ }
+
+ ///
+ /// Display information about the number of repeat patient visits in the last month.
+ ///
+ public int GetReturnVisitsCountLastMonth(){
+ var lastMonth = DateTime.Now.AddMonths(-1);
+ var startOfLastMonth = new DateTime(lastMonth.Year, lastMonth.Month, 1);
+ var startOfCurrentMonth = startOfLastMonth.AddMonths(1);
+
+ var appointments = _appointments.GetAllAppointments()
+ .Where(a => a.DateTime >= startOfLastMonth &&
+ a.DateTime < startOfCurrentMonth &&
+ a.IsReturnVisit)
+ .Count();
+
+ return appointments;
+ }
+
+ ///
+ /// Display information about patients over 30 years old who have appointments with multiple doctors, sorted by birth date.
+ ///
+ public IReadOnlyList GetPatientsOver30WithMultipleDoctorsOrderedByBirthDate(){
+ var thirtyYearsAgo = DateOnly.FromDateTime(DateTime.Now.AddYears(-30));
+
+ var appointments = _appointments.GetAllAppointments();
+
+ var patientsWithMultipleDoctors = appointments
+ .GroupBy(a => a.PatientId)
+ .Where(g => g.Select(a => a.DoctorId).Distinct().Count() > 1)
+ .Select(g => g.Key)
+ .ToList();
+
+ var patients = patientsWithMultipleDoctors
+ .Select(id => _patients.GetPatient(id))
+ .Where(p => p != null && p.BirthDate < thirtyYearsAgo)
+ .OrderBy(p => p!.BirthDate)
+ .ToList();
+
+ return _mapper.Map>(patients);
+ }
+
+ ///
+ /// Display information about appointments for the current month that are held in the selected room.
+ ///
+ public IReadOnlyList GetAppointmentsInRoomForCurrentMonth(int roomNumber){
+ var now = DateTime.Now;
+ var startOfMonth = new DateTime(now.Year, now.Month, 1);
+ var startOfNextMonth = startOfMonth.AddMonths(1);
+
+ var appointments = _appointments.GetAllAppointments()
+ .Where(a => a.RoomNumber == roomNumber &&
+ a.DateTime >= startOfMonth &&
+ a.DateTime < startOfNextMonth)
+ .OrderBy(a => a.DateTime)
+ .ToList();
+
+ return _mapper.Map>(appointments);
+ }
+}
\ No newline at end of file
diff --git a/Clinic/Clinic.Application.Services/Services/AppointmentServices.cs b/Clinic/Clinic.Application.Services/Services/AppointmentServices.cs
new file mode 100644
index 000000000..6b7f02cd4
--- /dev/null
+++ b/Clinic/Clinic.Application.Services/Services/AppointmentServices.cs
@@ -0,0 +1,149 @@
+using AutoMapper;
+using Clinic.Application.Ports;
+using Clinic.Application.DTOs.Appointment;
+using Clinic.Models.Entities;
+using Clinic.Application.Interfaces.Services;
+
+namespace Clinic.Application.Services;
+
+///
+/// Service layer for managing appointments in the clinic.
+/// Provides methods for creating, updating, retrieving, and deleting appointments.
+/// Handles mapping between DTOs and entity models, and interacts with the appointment database.
+///
+public class AppointmentServices : IAppointmentServices
+{
+ private readonly IAppointmentRepository _appointments;
+ private readonly IPatientRepository _patients;
+ private readonly IDoctorRepository _doctors;
+ private readonly IMapper _mapper;
+ private int _appointmentId;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The appointment database interface.
+ /// The patient database interface.
+ /// The doctor database interface.
+ /// The AutoMapper interface for DTO and entity mapping.
+ public AppointmentServices(
+ IAppointmentRepository appointments,
+ IPatientRepository patients,
+ IDoctorRepository doctors,
+ IMapper mapper)
+ {
+ _appointments = appointments;
+ _patients = patients;
+ _doctors = doctors;
+ _mapper = mapper;
+ _appointmentId = _appointments.AppointmentCount() + 1;
+ }
+
+ ///
+ /// Retrieves all appointments from the database.
+ ///
+ /// A read-only collection of appointment DTOs.
+ public IReadOnlyCollection GetAll()
+ {
+ var appointments = _appointments.GetAllAppointments();
+ return _mapper.Map>(appointments);
+ }
+
+ ///
+ /// Retrieves all appointments for a specific doctor.
+ ///
+ /// The identifier of the doctor.
+ /// A collection of appointment DTOs if the doctor exists; otherwise, null.
+ public IReadOnlyCollection? GetAppointmentsByDoctor(int doctorId)
+ {
+ var doctor = _doctors.GetDoctor(doctorId);
+ if (doctor == null)
+ {
+ return null;
+ }
+ var appointments = _appointments.GetAppointmentsByDoctor(doctorId);
+ return _mapper.Map>(appointments);
+ }
+
+ ///
+ /// Retrieves all appointments for a specific patient.
+ ///
+ /// The identifier of the patient.
+ /// A collection of appointment DTOs if the patient exists; otherwise, null.
+ public IReadOnlyCollection? GetAppointmentsByPatient(int patientId)
+ {
+ var patient = _patients.GetPatient(patientId);
+ if (patient == null)
+ {
+ return null;
+ }
+ var appointments = _appointments.GetAppointmentsByPatient(patientId);
+ return _mapper.Map>(appointments);
+ }
+
+ ///
+ /// Retrieves a single appointment by its identifier.
+ ///
+ /// The identifier of the appointment to retrieve.
+ /// The appointment as a DTO if found; otherwise, null.
+ public GetAppointmentDto? Get(int id)
+ {
+ var appointment = _appointments.GetAppointment(id);
+ return appointment == null ? null : _mapper.Map(appointment);
+ }
+
+ ///
+ /// Creates a new appointment entity in the database.
+ ///
+ /// The DTO containing appointment creation data.
+ /// The created appointment as a DTO if successful; otherwise, null.
+ public GetAppointmentDto? Create(CreateUpdateAppointmentDto dto)
+ {
+ var appointment = _mapper.Map(dto);
+ appointment.Id = _appointmentId;
+
+ if (!_appointments.AddAppointment(appointment))
+ {
+ return null;
+ }
+
+ _appointmentId++;
+ return _mapper.Map(appointment);
+ }
+
+ ///
+ /// Updates an existing appointment with the given identifier.
+ ///
+ /// The identifier of the appointment to update.
+ /// The DTO containing updated appointment data.
+ /// The updated appointment as a DTO if successful; otherwise, null.
+ public GetAppointmentDto? Update(int id, CreateUpdateAppointmentDto dto)
+ {
+ var appointment = _appointments.GetAppointment(id);
+ if (appointment == null)
+ {
+ return null;
+ }
+
+ _mapper.Map(dto, appointment);
+ _appointments.UpdateAppointment(appointment);
+
+ return _mapper.Map(appointment);
+ }
+
+ ///
+ /// Deletes an appointment from the database by its identifier.
+ ///
+ /// The identifier of the appointment to delete.
+ /// True if the appointment was successfully deleted; otherwise, false.
+ public bool Delete(int id)
+ {
+ if (!_appointments.RemoveAppointment(id))
+ {
+ return false;
+ }
+
+ _appointmentId--;
+ return true;
+ }
+}
diff --git a/Clinic/Clinic.Application.Services/Services/DoctorServices.cs b/Clinic/Clinic.Application.Services/Services/DoctorServices.cs
new file mode 100644
index 000000000..27dcac25a
--- /dev/null
+++ b/Clinic/Clinic.Application.Services/Services/DoctorServices.cs
@@ -0,0 +1,111 @@
+using AutoMapper;
+using Clinic.Application.Ports;
+using Clinic.Application.DTOs.Doctor;
+using Clinic.Models.Entities;
+using Clinic.Application.Interfaces.Services;
+
+namespace Clinic.Application.Services;
+
+///
+/// Service class for managing doctor-related operations within the Clinic API.
+/// Provides methods to create, retrieve, update, and delete doctors,
+/// as well as functions to get all doctors from the underlying database.
+/// Uses AutoMapper for mapping between entity and DTO objects.
+///
+public class DoctorServices : IDoctorServices
+{
+ private readonly IDoctorRepository _db;
+ private readonly IMapper _mapper;
+ private int _doctorId;
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Sets the initial doctor identifier based on the count in the database.
+ ///
+ /// The database service for doctor operations.
+ /// The AutoMapper instance used for object mapping.
+ public DoctorServices(IDoctorRepository db, IMapper mapper)
+ {
+ _db = db;
+ _mapper = mapper;
+ _doctorId = _db.DoctorCount() + 1;
+ }
+
+ ///
+ /// Retrieves all doctors from the database and maps them to DTOs.
+ ///
+ /// A collection of representing doctors.
+ public IReadOnlyCollection GetAll()
+ {
+ var doctors = _db.GetAllDoctors();
+ var doctorsDto = _mapper.Map>(doctors);
+ return doctorsDto;
+ }
+
+ ///
+ /// Creates a new doctor entity in the database.
+ ///
+ /// The DTO containing doctor creation data.
+ /// The created doctor as a if successful; otherwise, null.
+ public GetDoctorDto? Create(CreateUpdateDoctorDto createDoctorDto)
+ {
+ var doctor = _mapper.Map(createDoctorDto);
+ doctor.Id = _doctorId;
+ if (!_db.AddDoctor(doctor))
+ {
+ return null;
+ }
+ return _mapper.Map(doctor);
+ }
+
+ ///
+ /// Retrieves a single doctor by ID.
+ ///
+ /// The doctor identifier.
+ /// A if found; otherwise, null.
+ public GetDoctorDto? Get(int id)
+ {
+ var doctor = _db.GetDoctor(id);
+ if (doctor == null)
+ {
+ return null;
+ }
+ var doctorGetDto = _mapper.Map(doctor);
+ return doctorGetDto;
+ }
+
+ ///
+ /// Updates an existing doctor's information.
+ ///
+ /// The doctor identifier.
+ /// DTO with updated doctor details.
+ /// The updated doctor as a if successful; otherwise, null.
+ public GetDoctorDto? Update(int id, CreateUpdateDoctorDto updateDoctorDto)
+ {
+ var doctor = _db.GetDoctor(id);
+ if (doctor == null)
+ {
+ return null;
+ }
+ _mapper.Map(updateDoctorDto, doctor);
+ _db.UpdateDoctor(doctor);
+
+ var doctorGetDto = _mapper.Map(doctor);
+ return doctorGetDto;
+ }
+
+ ///
+ /// Deletes a doctor from the database by ID.
+ ///
+ /// The doctor identifier to delete.
+ /// True if the doctor was successfully deleted; otherwise, false.
+ public bool Delete(int id)
+ {
+ if (!_db.RemoveDoctor(id))
+ {
+ return false;
+ }
+ _doctorId--;
+ return true;
+ }
+}
diff --git a/Clinic/Clinic.Application.Services/Services/PatientServices.cs b/Clinic/Clinic.Application.Services/Services/PatientServices.cs
new file mode 100644
index 000000000..8d908acdb
--- /dev/null
+++ b/Clinic/Clinic.Application.Services/Services/PatientServices.cs
@@ -0,0 +1,112 @@
+using AutoMapper;
+using Clinic.Application.Ports;
+using Clinic.Application.DTOs.Patient;
+using Clinic.Models.Entities;
+using Clinic.Application.Interfaces.Services;
+
+namespace Clinic.Application.Services;
+
+///
+/// Service class for managing patient-related operations in the Clinic API.
+/// Provides methods for creating, retrieving, updating, and deleting patients,
+/// as well as listing all patients. Uses AutoMapper for entity-DTO mapping.
+///
+public class PatientServices : IPatientServices
+{
+ private readonly IPatientRepository _db;
+ private readonly IMapper _mapper;
+ private int _patientId;
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Sets the initial patient identifier based on the count in the database.
+ ///
+ /// The database service for patient operations.
+ /// The AutoMapper instance for mapping objects.
+ public PatientServices(IPatientRepository db, IMapper mapper)
+ {
+ _db = db;
+ _mapper = mapper;
+ _patientId = _db.PatientCount() + 1;
+ }
+
+ ///
+ /// Retrieves all patients from the database and maps them to DTOs.
+ ///
+ /// A collection of representing all patients.
+ public IReadOnlyCollection GetAll()
+ {
+ var patients = _db.GetAllPatients();
+ var patientsDto = _mapper.Map>(patients);
+ return patientsDto;
+ }
+
+ ///
+ /// Creates a new patient entity in the database.
+ ///
+ /// The DTO containing patient creation data.
+ /// The created patient as a if successful; otherwise, null.
+ public GetPatientDto? Create(CreateUpdatePatientDto patientCreateDto)
+ {
+ var patient = _mapper.Map(patientCreateDto);
+ patient.Id = _patientId;
+ if (!_db.AddPatient(patient))
+ {
+ return null;
+ }
+ _patientId++;
+ var patientGetDto = _mapper.Map(patient);
+ return patientGetDto;
+ }
+
+ ///
+ /// Updates an existing patient with the given id using the provided update DTO.
+ ///
+ /// The identifier of the patient to update.
+ /// The DTO containing updated patient data.
+ /// The updated patient as a if successful; otherwise, null.
+ public GetPatientDto? Update(int id, CreateUpdatePatientDto patientUpdateDto)
+ {
+ var patient = _db.GetPatient(id);
+ if (patient == null)
+ {
+ return null;
+ }
+ _mapper.Map(patientUpdateDto, patient);
+ _db.UpdatePatient(patient);
+
+ var patientGetDto = _mapper.Map(patient);
+ return patientGetDto;
+ }
+
+ ///
+ /// Retrieves a patient with the specified id.
+ ///
+ /// The identifier of the patient to retrieve.
+ /// The patient as a if found; otherwise, null.
+ public GetPatientDto? Get(int id)
+ {
+ var patient = _db.GetPatient(id);
+ if (patient == null)
+ {
+ return null;
+ }
+ var patientGetDto = _mapper.Map(patient);
+ return patientGetDto;
+ }
+
+ ///
+ /// Deletes the patient with the given id.
+ ///
+ /// The identifier of the patient to delete.
+ /// True if the patient was deleted; otherwise, false.
+ public bool Delete(int id)
+ {
+ if (!_db.RemovePatient(id))
+ {
+ return false;
+ }
+ _patientId--;
+ return true;
+ }
+}
diff --git a/Clinic/Clinic.Application.Services/Services/SpecializationServices.cs b/Clinic/Clinic.Application.Services/Services/SpecializationServices.cs
new file mode 100644
index 000000000..6f0a5fc75
--- /dev/null
+++ b/Clinic/Clinic.Application.Services/Services/SpecializationServices.cs
@@ -0,0 +1,107 @@
+using AutoMapper;
+using Clinic.Models.Entities;
+using Clinic.Application.Ports;
+using Clinic.Application.DTOs.Specialization;
+using Clinic.Application.Interfaces.Services;
+
+namespace Clinic.Application.Services;
+
+///
+/// Service class for managing specialization-related operations in the Clinic API.
+/// Provides methods to create, retrieve, and delete specializations, and maps entity objects to DTOs.
+///
+public class SpecializationServices : ISpecializationServices
+{
+ private readonly ISpecializationRepository _db;
+ private readonly IMapper _mapper;
+ private int _specializationId;
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Sets the initial specialization identifier based on the count in the database.
+ ///
+ /// The database service for specialization operations.
+ /// The AutoMapper instance used for object mapping.
+ public SpecializationServices(ISpecializationRepository db, IMapper mapper)
+ {
+ _db = db;
+ _mapper = mapper;
+ _specializationId = _db.SpecializationCount() + 1;
+ }
+
+ ///
+ /// Retrieves all specializations from the database and maps them to DTOs.
+ ///
+ /// A collection of representing specializations.
+ public IReadOnlyCollection GetAll()
+ {
+ var specializations = _db.GetAllSpecializations();
+ var specializationDtos = _mapper.Map>(specializations);
+ return specializationDtos;
+ }
+
+ ///
+ /// Updates an existing specialization with the given identifier.
+ ///
+ /// The identifier of the specialization to update.
+ /// The DTO containing updated specialization data.
+ /// The updated specialization as a DTO if successful; otherwise, null.
+ public GetSpecializationDto? Update(int id, CreateUpdateSpecializationDto updateSpecializationDto)
+ {
+ var specialization = _mapper.Map(updateSpecializationDto);
+ var updatedSpecialization = _db.UpdateSpecialization(id, specialization);
+ if (updatedSpecialization == null)
+ {
+ return null;
+ }
+ return _mapper.Map(updatedSpecialization);
+ }
+
+ ///
+ /// Creates a new specialization entity in the database.
+ ///
+ /// The DTO containing specialization creation data.
+ /// The created specialization as a if successful; otherwise, null.
+ public GetSpecializationDto? Create(CreateUpdateSpecializationDto createSpecializationDto)
+ {
+ var specialization = _mapper.Map(createSpecializationDto);
+ specialization.Id = _specializationId;
+
+ if (!_db.AddSpecialization(specialization))
+ {
+ return null;
+ }
+
+ var specializationDto = _mapper.Map(specialization);
+ specializationDto.Id = _specializationId;
+
+ _specializationId++;
+
+ return specializationDto;
+ }
+
+ ///
+ /// Retrieves a single specialization by ID.
+ ///
+ /// The specialization identifier.
+ /// A if found; otherwise, null.
+ public GetSpecializationDto? Get(int id)
+ {
+ var specialization = _db.GetSpecialization(id);
+ if (specialization == null)
+ {
+ return null;
+ }
+ return _mapper.Map(specialization);
+ }
+
+ ///
+ /// Deletes a specialization by ID.
+ ///
+ /// The specialization identifier.
+ /// True if the specialization was deleted; otherwise, false.
+ public bool Delete(int id)
+ {
+ return _db.RemoveSpecialization(id);
+ }
+}
diff --git a/Clinic/Clinic.Application/Clinic.Application.csproj b/Clinic/Clinic.Application/Clinic.Application.csproj
new file mode 100644
index 000000000..18a1b78a8
--- /dev/null
+++ b/Clinic/Clinic.Application/Clinic.Application.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/Clinic/Clinic.Application/DTOs/Appointment/CreateUpdateAppointmentDto.cs b/Clinic/Clinic.Application/DTOs/Appointment/CreateUpdateAppointmentDto.cs
new file mode 100644
index 000000000..27b2a73f7
--- /dev/null
+++ b/Clinic/Clinic.Application/DTOs/Appointment/CreateUpdateAppointmentDto.cs
@@ -0,0 +1,33 @@
+namespace Clinic.Application.DTOs.Appointment;
+
+///
+/// DTO for creating a new appointment, including required patient/doctor information,
+/// scheduled date/time, room number, and return visit flag.
+///
+public class CreateUpdateAppointmentDto
+{
+ ///
+ /// The full name of the patient for the appointment.
+ ///
+ public required string PatientFullName { get; set; }
+
+ ///
+ /// The full name of the doctor for the appointment.
+ ///
+ public required string DoctorFullName { get; set; }
+
+ ///
+ /// The date and time of the appointment.
+ ///
+ public required DateTime DateTime { get; set; }
+
+ ///
+ /// The room number where the appointment will take place.
+ ///
+ public required int RoomNumber { get; set; }
+
+ ///
+ /// Indicates whether the appointment is a return visit.
+ ///
+ public required bool IsReturnVisit { get; set; }
+}
diff --git a/Clinic/Clinic.Application/DTOs/Appointment/GetAppointmentDto.cs b/Clinic/Clinic.Application/DTOs/Appointment/GetAppointmentDto.cs
new file mode 100644
index 000000000..974efe0a8
--- /dev/null
+++ b/Clinic/Clinic.Application/DTOs/Appointment/GetAppointmentDto.cs
@@ -0,0 +1,49 @@
+namespace Clinic.Application.DTOs.Appointment;
+
+///
+/// DTO for retrieving detailed information about an appointment,
+/// including patient and doctor IDs/names, appointment date, room number, and return visit flag.
+///
+
+public class GetAppointmentDto
+{
+ ///
+ /// The unique identifier of the appointment.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// The ID of the patient associated with the appointment.
+ ///
+ public int PatientId { get; set; }
+
+ ///
+ /// The ID of the doctor associated with the appointment.
+ ///
+ public int DoctorId { get; set; }
+
+ ///
+ /// The full name of the patient.
+ ///
+ public string PatientFullName { get; set; } = null!;
+
+ ///
+ /// The full name of the doctor.
+ ///
+ public string DoctorFullName { get; set; } = null!;
+
+ ///
+ /// The date and time of the appointment.
+ ///
+ public DateTime DateTime { get; set; }
+
+ ///
+ /// The room number where the appointment takes place.
+ ///
+ public int RoomNumber { get; set; }
+
+ ///
+ /// Indicates whether this is a return visit for the patient.
+ ///
+ public bool IsReturnVisit { get; set; }
+}
diff --git a/Clinic/Clinic.Application/DTOs/Doctor/CreateUpdateDoctorDto.cs b/Clinic/Clinic.Application/DTOs/Doctor/CreateUpdateDoctorDto.cs
new file mode 100644
index 000000000..bdca43a40
--- /dev/null
+++ b/Clinic/Clinic.Application/DTOs/Doctor/CreateUpdateDoctorDto.cs
@@ -0,0 +1,55 @@
+using Clinic.Application.DTOs.Specialization;
+
+namespace Clinic.Application.DTOs.Doctor;
+
+///
+/// DTO for creating a new doctor, including required personal information,
+/// specialization list, and experience years.
+///
+public class CreateUpdateDoctorDto
+{
+ ///
+ /// Required: The passport number of the doctor.
+ ///
+ public required string PassportNumber { get; set; }
+
+ ///
+ /// Required: The birth date of the doctor.
+ ///
+ public required DateOnly BirthDate { get; set; }
+
+ ///
+ /// Required: The last name of the doctor.
+ ///
+ public required string LastName { get; set; }
+
+ ///
+ /// Required: The first name of the doctor.
+ ///
+ public required string FirstName { get; set; }
+
+ ///
+ /// Required: The phone number of the doctor.
+ ///
+ public required string PhoneNumber { get; set; }
+
+ ///
+ /// Optional: The patronymic (middle name) of the doctor.
+ ///
+ public string? Patronymic { get; set; }
+
+ ///
+ /// Required: The gender of the doctor.
+ ///
+ public required String Gender { get; set; }
+
+ ///
+ /// Required: The list of specializations for the doctor.
+ ///
+ public required List Specializations { get; set; }
+
+ ///
+ /// Required: The number of years of experience for the doctor.
+ ///
+ public required int ExperienceYears { get; set; }
+}
\ No newline at end of file
diff --git a/Clinic/Clinic.Application/DTOs/Doctor/GetDoctorDto.cs b/Clinic/Clinic.Application/DTOs/Doctor/GetDoctorDto.cs
new file mode 100644
index 000000000..354a20167
--- /dev/null
+++ b/Clinic/Clinic.Application/DTOs/Doctor/GetDoctorDto.cs
@@ -0,0 +1,53 @@
+namespace Clinic.Application.DTOs.Doctor;
+
+///
+/// DTO for retrieving detailed information about a doctor,
+/// including personal details, specialization list, and experience years.
+///
+public class GetDoctorDto
+{
+ ///
+ /// The unique identifier of the doctor.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// The passport number of the doctor.
+ ///
+ public string PassportNumber { get; set; } = string.Empty;
+
+ ///
+ /// The birth date of the doctor.
+ ///
+ public DateOnly BirthDate { get; set; }
+
+ ///
+ /// The last name of the doctor.
+ ///
+ public string LastName { get; set; } = string.Empty;
+
+ ///
+ /// The first name of the doctor.
+ ///
+ public string FirstName { get; set; } = string.Empty;
+
+ ///
+ /// Optional: The patronymic (middle name) of the doctor.
+ ///
+ public string? Patronymic { get; set; }
+
+ ///
+ /// The gender of the doctor.
+ ///
+ public String Gender { get; set; } = null!;
+
+ ///
+ /// The list of specializations for the doctor.
+ ///
+ public List Specializations { get; set; } = new();
+
+ ///
+ /// The number of years of experience for the doctor.
+ ///
+ public int ExperienceYears { get; set; }
+}
\ No newline at end of file
diff --git a/Clinic/Clinic.Application/DTOs/Patient/CreateUpdatePatientDto.cs b/Clinic/Clinic.Application/DTOs/Patient/CreateUpdatePatientDto.cs
new file mode 100644
index 000000000..380d87779
--- /dev/null
+++ b/Clinic/Clinic.Application/DTOs/Patient/CreateUpdatePatientDto.cs
@@ -0,0 +1,58 @@
+namespace Clinic.Application.DTOs.Patient;
+
+///
+/// DTO for creating a new patient, including required personal information,
+/// medical history, and contact details.
+///
+public class CreateUpdatePatientDto
+{
+ ///
+ /// The patient's first name.
+ ///
+ public required string FirstName { get; set; }
+
+ ///
+ /// The patient's last name.
+ ///
+ public required string LastName { get; set; }
+
+ ///
+ /// Optional patronymic (middle name) of the patient.
+ ///
+ public string? Patronymic { get; set; }
+
+ ///
+ /// The patient's passport number.
+ ///
+ public required string PassportNumber { get; set; }
+
+ ///
+ /// The patient's birth date.
+ ///
+ public required DateOnly BirthDate { get; set; }
+
+ ///
+ /// The patient's gender as a string (e.g. "Male", "Female").
+ ///
+ public required String Gender { get; set; }
+
+ ///
+ /// The patient's residential address.
+ ///
+ public required string Address { get; set; }
+
+ ///
+ /// The patient's contact phone number.
+ ///
+ public required string PhoneNumber { get; set; }
+
+ ///
+ /// The patient's blood group as a string (e.g. "A", "B", "AB", "O").
+ ///
+ public required String BloodGroup { get; set; }
+
+ ///
+ /// The patient's rhesus factor as a string (e.g. "Positive", "Negative").
+ ///
+ public required String RhesusFactor { get; set; }
+}
\ No newline at end of file
diff --git a/Clinic/Clinic.Application/DTOs/Patient/GetPatientDto.cs b/Clinic/Clinic.Application/DTOs/Patient/GetPatientDto.cs
new file mode 100644
index 000000000..c6fb978e7
--- /dev/null
+++ b/Clinic/Clinic.Application/DTOs/Patient/GetPatientDto.cs
@@ -0,0 +1,48 @@
+namespace Clinic.Application.DTOs.Patient;
+
+///
+/// DTO for retrieving detailed information about a patient,
+/// including personal details, medical history, and contact details.
+///
+public class GetPatientDto
+{
+ ///
+ /// The unique identifier of the patient.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// The patient's first name.
+ ///
+ public string FirstName { get; set; } = null!;
+
+ ///
+ /// The patient's last name.
+ ///
+ public string LastName { get; set; } = null!;
+
+ ///
+ /// Optional patronymic (middle name) of the patient.
+ ///
+ public string? Patronymic { get; set; }
+
+ ///
+ /// The patient's birth date.
+ ///
+ public DateOnly BirthDate { get; set; }
+
+ ///
+ /// The patient's gender as a string.
+ ///
+ public String Gender { get; set; } = null!;
+
+ ///
+ /// The patient's blood group as a string (A, B, AB, O).
+ ///
+ public String BloodGroup { get; set; } = null!;
+
+ ///
+ /// The patient's rhesus factor as a string (Positive/Negative).
+ ///
+ public String RhesusFactor { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/Clinic/Clinic.Application/DTOs/Specialization/CreateUpdateSpecializationDto.cs b/Clinic/Clinic.Application/DTOs/Specialization/CreateUpdateSpecializationDto.cs
new file mode 100644
index 000000000..ae9d97c90
--- /dev/null
+++ b/Clinic/Clinic.Application/DTOs/Specialization/CreateUpdateSpecializationDto.cs
@@ -0,0 +1,12 @@
+namespace Clinic.Application.DTOs.Specialization;
+
+///
+/// DTO for creating a new specialization, including required name.
+///
+public class CreateUpdateSpecializationDto
+{
+ ///
+ /// The name of the specialization to create.
+ ///
+ public required string Name { get; set; }
+}
\ No newline at end of file
diff --git a/Clinic/Clinic.Application/DTOs/Specialization/GetSpecializationDto.cs b/Clinic/Clinic.Application/DTOs/Specialization/GetSpecializationDto.cs
new file mode 100644
index 000000000..afb1ff261
--- /dev/null
+++ b/Clinic/Clinic.Application/DTOs/Specialization/GetSpecializationDto.cs
@@ -0,0 +1,18 @@
+namespace Clinic.Application.DTOs.Specialization;
+
+///
+/// DTO for retrieving detailed information about a specialization,
+/// including its unique identifier and name.
+///
+public class GetSpecializationDto
+{
+ ///
+ /// The unique identifier of the specialization.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// The name of the specialization.
+ ///
+ public string Name { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/Clinic/Clinic.Application/Interfaces/Services/IAppointmentService.cs b/Clinic/Clinic.Application/Interfaces/Services/IAppointmentService.cs
new file mode 100644
index 000000000..bf5df0f6e
--- /dev/null
+++ b/Clinic/Clinic.Application/Interfaces/Services/IAppointmentService.cs
@@ -0,0 +1,24 @@
+using Clinic.Application.DTOs.Appointment;
+
+namespace Clinic.Application.Interfaces.Services;
+
+///
+/// Interface for appointment service operations.
+/// Provides methods for managing appointments in the clinic system.
+///
+public interface IAppointmentServices : IBaseServices
+{
+ ///
+ /// Retrieves all appointments for a specific doctor.
+ ///
+ /// The identifier of the doctor.
+ /// A collection of appointment DTOs if the doctor exists; otherwise, null.
+ public IReadOnlyCollection? GetAppointmentsByDoctor(int doctorId);
+
+ ///
+ /// Retrieves all appointments for a specific patient.
+ ///
+ /// The identifier of the patient.
+ /// A collection of appointment DTOs if the patient exists; otherwise, null.
+ public IReadOnlyCollection? GetAppointmentsByPatient(int patientId);
+}
diff --git a/Clinic/Clinic.Application/Interfaces/Services/IBaseService.cs b/Clinic/Clinic.Application/Interfaces/Services/IBaseService.cs
new file mode 100644
index 000000000..ba4fb107b
--- /dev/null
+++ b/Clinic/Clinic.Application/Interfaces/Services/IBaseService.cs
@@ -0,0 +1,48 @@
+namespace Clinic.Application.Interfaces.Services;
+
+///
+/// Generic interface for application services that provides CRUD operations.
+/// This interface can be used to type all service functions with specific DTOs and entities.
+///
+/// The entity type used in the database layer.
+/// The DTO type for retrieving entities.
+/// The DTO type for create and update operations.
+public interface IBaseServices
+ where TGetDto : class
+ where TSaveDto : class
+{
+ ///
+ /// Retrieves all entities from the database and maps them to DTOs.
+ ///
+ /// A read-only collection of DTOs representing all entities.
+ public IReadOnlyCollection GetAll();
+
+ ///
+ /// Retrieves a single entity by its identifier.
+ ///
+ /// The identifier of the entity to retrieve.
+ /// The entity as a DTO if found; otherwise, null.
+ public TGetDto? Get(int id);
+
+ ///
+ /// Creates a new entity in the database.
+ ///
+ /// The DTO containing entity creation data.
+ /// The created entity as a DTO if successful; otherwise, null.
+ public TGetDto? Create(TSaveDto createDto);
+
+ ///
+ /// Updates an existing entity with the given identifier.
+ ///
+ /// The identifier of the entity to update.
+ /// The DTO containing updated entity data.
+ /// The updated entity as a DTO if successful; otherwise, null.
+ public TGetDto? Update(int id, TSaveDto updateDto);
+
+ ///
+ /// Deletes an entity from the database by its identifier.
+ ///
+ /// The identifier of the entity to delete.
+ /// True if the entity was successfully deleted; otherwise, false.
+ public bool Delete(int id);
+}
diff --git a/Clinic/Clinic.Application/Interfaces/Services/IDoctorService.cs b/Clinic/Clinic.Application/Interfaces/Services/IDoctorService.cs
new file mode 100644
index 000000000..b342c5a2f
--- /dev/null
+++ b/Clinic/Clinic.Application/Interfaces/Services/IDoctorService.cs
@@ -0,0 +1,9 @@
+using Clinic.Application.DTOs.Doctor;
+
+namespace Clinic.Application.Interfaces.Services;
+
+///
+/// Interface for doctor service operations.
+/// Provides methods for managing doctors in the clinic system.
+///
+public interface IDoctorServices : IBaseServices;
diff --git a/Clinic/Clinic.Application/Interfaces/Services/IPatientService.cs b/Clinic/Clinic.Application/Interfaces/Services/IPatientService.cs
new file mode 100644
index 000000000..df7aaa9ad
--- /dev/null
+++ b/Clinic/Clinic.Application/Interfaces/Services/IPatientService.cs
@@ -0,0 +1,9 @@
+using Clinic.Application.DTOs.Patient;
+
+namespace Clinic.Application.Interfaces.Services;
+
+///
+/// Interface for patient service operations.
+/// Provides methods for managing patients in the clinic system.
+///
+public interface IPatientServices : IBaseServices;
diff --git a/Clinic/Clinic.Application/Interfaces/Services/ISpecializationService.cs b/Clinic/Clinic.Application/Interfaces/Services/ISpecializationService.cs
new file mode 100644
index 000000000..008b08c0e
--- /dev/null
+++ b/Clinic/Clinic.Application/Interfaces/Services/ISpecializationService.cs
@@ -0,0 +1,10 @@
+using Clinic.Application.DTOs.Specialization;
+
+namespace Clinic.Application.Interfaces.Services;
+
+///
+/// Interface for specialization service operations.
+/// Provides methods for managing specializations in the clinic system.
+/// Note: Specializations do not support update operations.
+///
+public interface ISpecializationServices : IBaseServices;
diff --git a/Clinic/Clinic.Application/Ports/IAppointmentRepository.cs b/Clinic/Clinic.Application/Ports/IAppointmentRepository.cs
new file mode 100644
index 000000000..c8f281194
--- /dev/null
+++ b/Clinic/Clinic.Application/Ports/IAppointmentRepository.cs
@@ -0,0 +1,52 @@
+using Clinic.Models.Entities;
+
+namespace Clinic.Application.Ports;
+
+///
+/// Abstraction for appointment persistence operations.
+/// Provides methods to query and modify appointments in the data store.
+///
+public interface IAppointmentRepository
+{
+ ///
+ /// Retrieves an appointment by identifier.
+ ///
+ public Appointment? GetAppointment(int id);
+
+ ///
+ /// Returns all appointments.
+ ///
+ public IReadOnlyCollection GetAllAppointments();
+
+ ///
+ /// Returns appointments for the specified doctor.
+ ///
+ public IReadOnlyCollection GetAppointmentsByDoctor(int doctorId);
+
+ ///
+ /// Returns appointments for the specified patient.
+ ///
+ public IReadOnlyCollection GetAppointmentsByPatient(int patientId);
+
+ ///
+ /// Adds a new appointment. Returns true on success.
+ ///
+ public bool AddAppointment(Appointment appointment);
+
+ ///
+ /// Updates an appointment. Returns true if updated.
+ ///
+ public bool UpdateAppointment(Appointment appointment);
+
+ ///
+ /// Removes an appointment by id. Returns true if removed.
+ ///
+ public bool RemoveAppointment(int id);
+
+ ///
+ /// Returns total number of appointments.
+ ///
+ public int AppointmentCount();
+}
+
+
diff --git a/Clinic/Clinic.Application/Ports/IDoctorRepository.cs b/Clinic/Clinic.Application/Ports/IDoctorRepository.cs
new file mode 100644
index 000000000..4a7367c7e
--- /dev/null
+++ b/Clinic/Clinic.Application/Ports/IDoctorRepository.cs
@@ -0,0 +1,42 @@
+using Clinic.Models.Entities;
+
+namespace Clinic.Application.Ports;
+
+///
+/// Abstraction for doctor persistence operations.
+/// Implementations should provide CRUD operations for doctor entities.
+///
+public interface IDoctorRepository
+{
+ ///
+ /// Retrieves a doctor by identifier, or null if not found.
+ ///
+ public Doctor? GetDoctor(int id);
+
+ ///
+ /// Returns all doctors.
+ ///
+ public IReadOnlyCollection GetAllDoctors();
+
+ ///
+ /// Adds a new doctor. Returns true on success.
+ ///
+ public bool AddDoctor(Doctor doctor);
+
+ ///
+ /// Updates an existing doctor. Returns true if updated.
+ ///
+ public bool UpdateDoctor(Doctor doctor);
+
+ ///
+ /// Removes a doctor by id. Returns true if removed.
+ ///
+ public bool RemoveDoctor(int id);
+
+ ///
+ /// Returns the total count of doctors.
+ ///
+ public int DoctorCount();
+}
+
+
diff --git a/Clinic/Clinic.Application/Ports/IPatientRepository.cs b/Clinic/Clinic.Application/Ports/IPatientRepository.cs
new file mode 100644
index 000000000..539f0f571
--- /dev/null
+++ b/Clinic/Clinic.Application/Ports/IPatientRepository.cs
@@ -0,0 +1,42 @@
+using Clinic.Models.Entities;
+
+namespace Clinic.Application.Ports;
+
+///
+/// Abstraction for patient persistence operations.
+/// Implementations should provide methods to get, add, update and remove patients.
+///
+public interface IPatientRepository
+{
+ ///
+ /// Retrieves a patient by identifier.
+ ///
+ public Patient? GetPatient(int id);
+
+ ///
+ /// Returns all patients.
+ ///
+ public IReadOnlyCollection GetAllPatients();
+
+ ///
+ /// Adds a new patient. Returns true on success.
+ ///
+ public bool AddPatient(Patient patient);
+
+ ///
+ /// Updates an existing patient. Returns true if updated.
+ ///
+ public bool UpdatePatient(Patient patient);
+
+ ///
+ /// Removes a patient by id. Returns true if removed.
+ ///
+ public bool RemovePatient(int id);
+
+ ///
+ /// Returns the total count of patients.
+ ///
+ public int PatientCount();
+}
+
+
diff --git a/Clinic/Clinic.Application/Ports/ISpecializationRepository.cs b/Clinic/Clinic.Application/Ports/ISpecializationRepository.cs
new file mode 100644
index 000000000..d07b612c0
--- /dev/null
+++ b/Clinic/Clinic.Application/Ports/ISpecializationRepository.cs
@@ -0,0 +1,42 @@
+using Clinic.Models.Entities;
+
+namespace Clinic.Application.Ports;
+
+///
+/// Abstraction for specialization persistence operations.
+/// Provides methods to query and modify specializations.
+///
+public interface ISpecializationRepository
+{
+ ///
+ /// Returns all specializations.
+ ///
+ public IReadOnlyCollection GetAllSpecializations();
+
+ ///
+ /// Adds a new specialization. Returns true on success.
+ ///
+ public bool AddSpecialization(Specialization specialization);
+
+ ///
+ /// Updates an existing specialization and returns the updated entity, or null if not found.
+ ///
+ public Specialization? UpdateSpecialization(int id, Specialization specialization);
+
+ ///
+ /// Retrieves a specialization by identifier.
+ ///
+ public Specialization? GetSpecialization(int id);
+
+ ///
+ /// Removes a specialization by id. Returns true if removed.
+ ///
+ public bool RemoveSpecialization(int id);
+
+ ///
+ /// Returns total number of specializations.
+ ///
+ public int SpecializationCount();
+}
+
+
diff --git a/Clinic/Clinic.Application/Properties/launchSettings.json b/Clinic/Clinic.Application/Properties/launchSettings.json
new file mode 100644
index 000000000..95a174177
--- /dev/null
+++ b/Clinic/Clinic.Application/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5233",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7077;http://localhost:5233",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/Clinic/Clinic.AppointmentGenerator/Clinic.AppointmentGenerator.csproj b/Clinic/Clinic.AppointmentGenerator/Clinic.AppointmentGenerator.csproj
new file mode 100644
index 000000000..4cef0fdc8
--- /dev/null
+++ b/Clinic/Clinic.AppointmentGenerator/Clinic.AppointmentGenerator.csproj
@@ -0,0 +1,27 @@
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
diff --git a/Clinic/Clinic.AppointmentGenerator/Program.cs b/Clinic/Clinic.AppointmentGenerator/Program.cs
new file mode 100644
index 000000000..3bf620f21
--- /dev/null
+++ b/Clinic/Clinic.AppointmentGenerator/Program.cs
@@ -0,0 +1,180 @@
+using System.Globalization;
+using Clinic.Contracts;
+using Grpc.Net.Client;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+using var loggerFactory = LoggerFactory.Create(builder =>
+{
+ builder
+ .SetMinimumLevel(LogLevel.Information)
+ .AddSimpleConsole(options =>
+ {
+ options.SingleLine = true;
+ options.TimestampFormat = "HH:mm:ss ";
+ });
+});
+var logger = loggerFactory.CreateLogger("Clinic.AppointmentGenerator");
+
+var configuration = new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json", optional: true)
+ .AddEnvironmentVariables()
+ .Build();
+
+var endpoint = configuration["Grpc:Endpoint"];
+if (string.IsNullOrWhiteSpace(endpoint))
+{
+ logger.LogError("Missing Grpc:Endpoint configuration.");
+ return 1;
+}
+
+var aspireHttpsEndpoint = Environment.GetEnvironmentVariable("services__clinic-api__https__0");
+var aspireHttpEndpoint = Environment.GetEnvironmentVariable("services__clinic-api__http__0");
+if (endpoint.Contains("clinic-api", StringComparison.OrdinalIgnoreCase))
+{
+ endpoint = aspireHttpsEndpoint ?? aspireHttpEndpoint ?? endpoint;
+}
+
+if (endpoint.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
+{
+ logger.LogWarning("Using insecure gRPC endpoint {Endpoint}. Enabling Http2UnencryptedSupport.", endpoint);
+ AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
+}
+
+var count = configuration.GetValue("Generator:Count", 10);
+var roomNumbers = configuration.GetSection("Generator:RoomNumbers").Get() ?? [101, 102, 201, 202];
+var patients = configuration.GetSection("Generator:Patients").Get() ??
+[
+ new PersonSeed(1, "Иванов Иван Иванович"),
+ new PersonSeed(2, "Петров Петр Петрович"),
+ new PersonSeed(3, "Сидорова Мария Алексеевна")
+];
+var doctors = configuration.GetSection("Generator:Doctors").Get() ??
+[
+ new PersonSeed(1, "Смирнов Алексей Николаевич"),
+ new PersonSeed(2, "Кузнецова Елена Сергеевна"),
+ new PersonSeed(3, "Волков Дмитрий Олегович")
+];
+
+var knownVisitPairs = new HashSet<(int PatientId, int DoctorId)>();
+
+if (count <= 0)
+{
+ logger.LogError("Generator:Count must be greater than 0.");
+ return 1;
+}
+
+if (roomNumbers.Length == 0)
+{
+ logger.LogError("Generator:RoomNumbers is empty.");
+ return 1;
+}
+
+if (patients.Length == 0 || doctors.Length == 0)
+{
+ logger.LogError("Generator:Patients and Generator:Doctors must contain at least one item.");
+ return 1;
+}
+
+logger.LogInformation(
+ "Generator started. Endpoint={Endpoint}; Count={Count}; Patients={PatientsCount}; Doctors={DoctorsCount}.",
+ endpoint,
+ count,
+ patients.Length,
+ doctors.Length);
+
+//Create gRPC channel and client, then send generated contracts with retry logic for endpoint availability.
+const int grpcAttempts = 10;
+for (var attempt = 1; attempt <= grpcAttempts; attempt++)
+{
+ try
+ {
+ using var channel = GrpcChannel.ForAddress(endpoint);
+ var client = new ContractIngestor.ContractIngestorClient(channel);
+
+ using var call = client.IngestContracts();
+ var random = new Random();
+
+ for (var i = 0; i < count; i++)
+ {
+ var patient = patients[random.Next(patients.Length)];
+ var doctor = doctors[random.Next(doctors.Length)];
+ var contract = BuildRandomContract(patient, doctor, roomNumbers, random, knownVisitPairs);
+
+ await call.RequestStream.WriteAsync(contract);
+ logger.LogInformation(
+ "Sent contract {Index}/{Total}: PatientId={PatientId}; DoctorId={DoctorId}; Room={Room}; IsReturnVisit={IsReturnVisit}; Time={Time}.",
+ i + 1,
+ count,
+ contract.PatientId,
+ contract.DoctorId,
+ contract.RoomNumber,
+ contract.IsReturnVisit,
+ contract.AppointmentTime);
+ }
+
+ await call.RequestStream.CompleteAsync();
+
+ var result = await call;
+ logger.LogInformation("Ingest result: Received={Received}; Saved={Saved}; Failed={Failed}.", result.Received, result.Saved, result.Failed);
+ foreach (var error in result.Errors)
+ {
+ logger.LogWarning("Ingest error: {Error}", error);
+ }
+
+ return 0;
+ }
+ catch (Grpc.Core.RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.Unavailable && attempt < grpcAttempts)
+ {
+ logger.LogWarning("gRPC endpoint is not ready yet (attempt {Attempt}/{MaxAttempts}). Retrying...", attempt, grpcAttempts);
+ await Task.Delay(TimeSpan.FromSeconds(2));
+ }
+}
+
+logger.LogError("Failed to send contracts to gRPC endpoint after retries.");
+return 1;
+
+///
+/// Creates a randomized appointment contract for gRPC streaming.
+///
+/// Source patient identity tuple.
+/// Source doctor identity tuple.
+/// Available room numbers.
+/// Random generator instance.
+/// Known patient-doctor pairs that already had visits.
+/// Prepared with randomized date and flags.
+static Contract BuildRandomContract(
+ PersonSeed patient,
+ PersonSeed doctor,
+ int[] roomNumbers,
+ Random random,
+ HashSet<(int PatientId, int DoctorId)> knownVisitPairs)
+{
+ var pair = (patient.Id, doctor.Id);
+ var isReturnVisit = knownVisitPairs.Contains(pair);
+ knownVisitPairs.Add(pair);
+
+ var appointmentTime = DateTimeOffset.UtcNow
+ .AddDays(random.Next(0, 30))
+ .AddHours(random.Next(8, 18))
+ .AddMinutes(random.Next(0, 60))
+ .ToString("O", CultureInfo.InvariantCulture);
+
+ return new Contract
+ {
+ PatientId = patient.Id,
+ PatientFullName = patient.FullName,
+ DoctorId = doctor.Id,
+ DoctorFullName = doctor.FullName,
+ AppointmentTime = appointmentTime,
+ RoomNumber = roomNumbers[random.Next(roomNumbers.Length)],
+ IsReturnVisit = isReturnVisit
+ };
+}
+
+///
+/// Seed entity used by the autonomous generator to avoid direct database dependency.
+///
+/// Domain identifier used in generated contracts.
+/// Full name included in generated contracts.
+readonly record struct PersonSeed(int Id, string FullName);
diff --git a/Clinic/Clinic.AppointmentGenerator/appsettings.json b/Clinic/Clinic.AppointmentGenerator/appsettings.json
new file mode 100644
index 000000000..da85bfcb1
--- /dev/null
+++ b/Clinic/Clinic.AppointmentGenerator/appsettings.json
@@ -0,0 +1,37 @@
+{
+ "Grpc": {
+ "Endpoint": "https://clinic-api"
+ },
+ "Generator": {
+ "Count": 10,
+ "RoomNumbers": [101, 102, 201, 202],
+ "Patients": [
+ {
+ "Id": 1,
+ "FullName": "Иванов Иван Иванович"
+ },
+ {
+ "Id": 2,
+ "FullName": "Петров Петр Петрович"
+ },
+ {
+ "Id": 3,
+ "FullName": "Сидорова Мария Алексеевна"
+ }
+ ],
+ "Doctors": [
+ {
+ "Id": 1,
+ "FullName": "Смирнов Алексей Николаевич"
+ },
+ {
+ "Id": 2,
+ "FullName": "Кузнецова Елена Сергеевна"
+ },
+ {
+ "Id": 3,
+ "FullName": "Волков Дмитрий Олегович"
+ }
+ ]
+ }
+}
diff --git a/Clinic/Clinic.Contracts/Clinic.Contracts.csproj b/Clinic/Clinic.Contracts/Clinic.Contracts.csproj
new file mode 100644
index 000000000..f4556af88
--- /dev/null
+++ b/Clinic/Clinic.Contracts/Clinic.Contracts.csproj
@@ -0,0 +1,19 @@
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+ all
+
+
+
+
+
+
+
diff --git a/Clinic/Clinic.Contracts/Proto/appointment.proto b/Clinic/Clinic.Contracts/Proto/appointment.proto
new file mode 100644
index 000000000..98d156a91
--- /dev/null
+++ b/Clinic/Clinic.Contracts/Proto/appointment.proto
@@ -0,0 +1,26 @@
+syntax = "proto3";
+
+package clinic.contracts;
+
+option csharp_namespace = "Clinic.Contracts";
+
+message Contract {
+ int32 patient_id = 1;
+ string patient_full_name = 2;
+ int32 doctor_id = 3;
+ string doctor_full_name = 4;
+ string appointment_time = 5; // ISO-8601 string
+ int32 room_number = 6;
+ bool is_return_visit = 7;
+}
+
+message IngestResult {
+ int32 received = 1;
+ int32 saved = 2;
+ int32 failed = 3;
+ repeated string errors = 4;
+}
+
+service ContractIngestor {
+ rpc IngestContracts (stream Contract) returns (IngestResult);
+}
diff --git a/Clinic/Clinic.DataBase/Clinic.DataBase.csproj b/Clinic/Clinic.DataBase/Clinic.DataBase.csproj
new file mode 100644
index 000000000..84ced4c32
--- /dev/null
+++ b/Clinic/Clinic.DataBase/Clinic.DataBase.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Clinic/Clinic.DataBase/ClinicDbContext.cs b/Clinic/Clinic.DataBase/ClinicDbContext.cs
new file mode 100644
index 000000000..f3548b319
--- /dev/null
+++ b/Clinic/Clinic.DataBase/ClinicDbContext.cs
@@ -0,0 +1,108 @@
+using Clinic.Models.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace Clinic.DataBase;
+
+///
+/// EF Core for the Clinic application. Configures entities
+/// and relationships for patients, doctors, specializations and appointments.
+///
+public class ClinicDbContext : DbContext
+{
+ public ClinicDbContext(DbContextOptions options) : base(options){}
+
+ ///
+ /// DbSet of patients.
+ ///
+ public DbSet Patients => Set();
+
+ ///
+ /// DbSet of doctors.
+ ///
+ public DbSet Doctors => Set();
+
+ ///
+ /// DbSet of specializations.
+ ///
+ public DbSet Specializations => Set();
+
+ ///
+ /// DbSet of appointments.
+ ///
+ public DbSet Appointments => Set();
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ // Configure Patient entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+ entity.Property(e => e.PassportNumber).IsRequired().HasMaxLength(50);
+ entity.Property(e => e.BirthDate).IsRequired();
+ entity.Property(e => e.LastName).IsRequired().HasMaxLength(100);
+ entity.Property(e => e.FirstName).IsRequired().HasMaxLength(100);
+ entity.Property(e => e.Patronymic).HasMaxLength(100);
+ entity.Property(e => e.PhoneNumber).IsRequired().HasMaxLength(20);
+ entity.Property(e => e.Gender).IsRequired().HasConversion();
+ entity.Property(e => e.Address).IsRequired().HasMaxLength(200);
+ entity.Property(e => e.BloodGroup).IsRequired().HasConversion();
+ entity.Property(e => e.RhesusFactor).IsRequired().HasConversion();
+ });
+
+ // Configure Doctor entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+ entity.Property(e => e.PassportNumber).IsRequired().HasMaxLength(50);
+ entity.Property(e => e.BirthDate).IsRequired();
+ entity.Property(e => e.LastName).IsRequired().HasMaxLength(100);
+ entity.Property(e => e.FirstName).IsRequired().HasMaxLength(100);
+ entity.Property(e => e.Patronymic).HasMaxLength(100);
+ entity.Property(e => e.PhoneNumber).IsRequired().HasMaxLength(20);
+ entity.Property(e => e.Gender).IsRequired().HasConversion();
+ entity.Property(e => e.ExperienceYears).IsRequired();
+
+ // Configure many-to-many relationship with Specialization
+ entity.HasMany(d => d.Specializations)
+ .WithMany()
+ .UsingEntity>(
+ "DoctorSpecialization",
+ j => j.HasOne().WithMany().HasForeignKey("SpecializationId"),
+ j => j.HasOne().WithMany().HasForeignKey("DoctorId"),
+ j => j.HasKey("DoctorId", "SpecializationId"));
+ });
+
+ // Configure Specialization entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+ entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
+ });
+
+ // Configure Appointment entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+ entity.Property(e => e.PatientId).IsRequired();
+ entity.Property(e => e.PatientFullName).IsRequired().HasMaxLength(300);
+ entity.Property(e => e.DoctorId).IsRequired();
+ entity.Property(e => e.DoctorFullName).IsRequired().HasMaxLength(300);
+ entity.Property(e => e.DateTime).IsRequired();
+ entity.Property(e => e.RoomNumber).IsRequired();
+ entity.Property(e => e.IsReturnVisit).IsRequired();
+
+ // Configure foreign key relationships
+ entity.HasOne()
+ .WithMany()
+ .HasForeignKey(a => a.PatientId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.HasOne()
+ .WithMany()
+ .HasForeignKey(a => a.DoctorId)
+ .OnDelete(DeleteBehavior.Restrict);
+ });
+ }
+}
+
+
diff --git a/Clinic/Clinic.DataBase/ClinicDbFactory.cs b/Clinic/Clinic.DataBase/ClinicDbFactory.cs
new file mode 100644
index 000000000..d251bd5ac
--- /dev/null
+++ b/Clinic/Clinic.DataBase/ClinicDbFactory.cs
@@ -0,0 +1,41 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.Extensions.Configuration;
+
+namespace Clinic.DataBase;
+
+///
+/// Factory used at design-time to create instances of .
+/// This is used by EF Core tools (migrations, scaffolding) to create a context.
+///
+public class ClinicDbFactory : IDesignTimeDbContextFactory
+{
+
+ ///
+ /// Creates a new for design-time operations.
+ ///
+ /// Command-line arguments (unused).
+ /// A configured instance.
+ public ClinicDbContext CreateDbContext(string[] args)
+ {
+ var configuration = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddJsonFile("appsettings.json")
+ .Build();
+
+ var connectionString = configuration.GetConnectionString("ClinicDb");
+
+ if (string.IsNullOrEmpty(connectionString))
+ {
+ throw new InvalidOperationException("Connection string 'ClinicDb' not found.");
+ }
+
+ var optionsBuilder = new DbContextOptionsBuilder();
+ optionsBuilder.UseNpgsql(
+ connectionString
+ );
+
+ return new ClinicDbContext(optionsBuilder.Options);
+ }
+
+}
\ No newline at end of file
diff --git a/Clinic/Clinic.DataBase/EntityFramework/EfAppointmentRepository.cs b/Clinic/Clinic.DataBase/EntityFramework/EfAppointmentRepository.cs
new file mode 100644
index 000000000..d26efb17b
--- /dev/null
+++ b/Clinic/Clinic.DataBase/EntityFramework/EfAppointmentRepository.cs
@@ -0,0 +1,106 @@
+using Clinic.Models.Entities;
+using Clinic.Application.Ports;
+using Microsoft.EntityFrameworkCore;
+
+namespace Clinic.DataBase.EntityFramework;
+
+///
+/// Entity Framework implementation of that manages
+/// appointment entities using a .
+///
+public sealed class EfAppointmentRepository : IAppointmentRepository
+{
+ private readonly ClinicDbContext _context;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The database context used for data access.
+ public EfAppointmentRepository(ClinicDbContext context)
+ {
+ _context = context;
+ }
+
+ ///
+ /// Retrieves an appointment by identifier.
+ ///
+ public Appointment? GetAppointment(int id) =>
+ _context.Appointments.Find(id);
+
+ ///
+ /// Returns all appointments as a read-only collection.
+ ///
+ public IReadOnlyCollection GetAllAppointments() =>
+ _context.Appointments.AsNoTracking().ToList();
+
+ ///
+ /// Returns appointments for the specified doctor.
+ ///
+ public IReadOnlyCollection GetAppointmentsByDoctor(int doctorId) =>
+ _context.Appointments
+ .Where(a => a.DoctorId == doctorId)
+ .AsNoTracking()
+ .ToList();
+
+ ///
+ /// Returns appointments for the specified patient.
+ ///
+ public IReadOnlyCollection GetAppointmentsByPatient(int patientId) =>
+ _context.Appointments
+ .Where(a => a.PatientId == patientId)
+ .AsNoTracking()
+ .ToList();
+
+ ///
+ /// Adds a new appointment. Returns true on success.
+ ///
+ public bool AddAppointment(Appointment appointment)
+ {
+ if (_context.Appointments.Any(a => a.Id == appointment.Id))
+ {
+ return false;
+ }
+
+ _context.Appointments.Add(appointment);
+ _context.SaveChanges();
+ return true;
+ }
+
+ ///
+ /// Updates an existing appointment. Returns true if updated.
+ ///
+ public bool UpdateAppointment(Appointment appointment)
+ {
+ if (!_context.Appointments.Any(a => a.Id == appointment.Id))
+ {
+ return false;
+ }
+
+ _context.Appointments.Update(appointment);
+ _context.SaveChanges();
+ return true;
+ }
+
+ ///
+ /// Removes an appointment by id. Returns true if removed.
+ ///
+ public bool RemoveAppointment(int id)
+ {
+ var entity = _context.Appointments.Find(id);
+ if (entity is null)
+ {
+ return false;
+ }
+
+ _context.Appointments.Remove(entity);
+ _context.SaveChanges();
+ return true;
+ }
+
+ ///
+ /// Returns total number of appointments.
+ ///
+ public int AppointmentCount() => _context.Appointments.Count();
+}
+
+
diff --git a/Clinic/Clinic.DataBase/EntityFramework/EfDoctorRepository.cs b/Clinic/Clinic.DataBase/EntityFramework/EfDoctorRepository.cs
new file mode 100644
index 000000000..e3e8d198b
--- /dev/null
+++ b/Clinic/Clinic.DataBase/EntityFramework/EfDoctorRepository.cs
@@ -0,0 +1,93 @@
+using Clinic.Models.Entities;
+using Clinic.Application.Ports;
+using Microsoft.EntityFrameworkCore;
+
+namespace Clinic.DataBase.EntityFramework;
+
+///
+/// Entity Framework implementation of that manages
+/// doctor entities via .
+///
+public sealed class EfDoctorRepository : IDoctorRepository
+{
+ private readonly ClinicDbContext _context;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The database context used for data access.
+ public EfDoctorRepository(ClinicDbContext context)
+ {
+ _context = context;
+ }
+
+ ///
+ /// Retrieves a doctor by identifier, including specializations.
+ ///
+ public Doctor? GetDoctor(int id) =>
+ _context.Doctors
+ .Include(d => d.Specializations)
+ .FirstOrDefault(d => d.Id == id);
+
+ ///
+ /// Returns all doctors including their specializations.
+ ///
+ public IReadOnlyCollection GetAllDoctors() =>
+ _context.Doctors
+ .Include(d => d.Specializations)
+ .AsNoTracking()
+ .ToList();
+
+ ///
+ /// Adds a new doctor. Returns true on success.
+ ///
+ public bool AddDoctor(Doctor doctor)
+ {
+ if (_context.Doctors.Any(d => d.Id == doctor.Id))
+ {
+ return false;
+ }
+
+ _context.Doctors.Add(doctor);
+ _context.SaveChanges();
+ return true;
+ }
+
+ ///
+ /// Updates an existing doctor. Returns true if updated.
+ ///
+ public bool UpdateDoctor(Doctor doctor)
+ {
+ if (!_context.Doctors.Any(d => d.Id == doctor.Id))
+ {
+ return false;
+ }
+
+ _context.Doctors.Update(doctor);
+ _context.SaveChanges();
+ return true;
+ }
+
+ ///
+ /// Removes a doctor by id. Returns true if removed.
+ ///
+ public bool RemoveDoctor(int id)
+ {
+ var entity = _context.Doctors.Find(id);
+ if (entity is null)
+ {
+ return false;
+ }
+
+ _context.Doctors.Remove(entity);
+ _context.SaveChanges();
+ return true;
+ }
+
+ ///
+ /// Returns total count of doctors.
+ ///
+ public int DoctorCount() => _context.Doctors.Count();
+}
+
+
diff --git a/Clinic/Clinic.DataBase/EntityFramework/EfPatientRepository.cs b/Clinic/Clinic.DataBase/EntityFramework/EfPatientRepository.cs
new file mode 100644
index 000000000..086f183a8
--- /dev/null
+++ b/Clinic/Clinic.DataBase/EntityFramework/EfPatientRepository.cs
@@ -0,0 +1,87 @@
+using Clinic.Models.Entities;
+using Clinic.Application.Ports;
+using Microsoft.EntityFrameworkCore;
+
+namespace Clinic.DataBase.EntityFramework;
+
+///
+/// Entity Framework implementation of that manages
+/// patient entities using a .
+///
+public sealed class EfPatientRepository : IPatientRepository
+{
+ private readonly ClinicDbContext _context;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The database context used for data access.
+ public EfPatientRepository(ClinicDbContext context)
+ {
+ _context = context;
+ }
+
+ ///
+ /// Retrieves a patient by identifier.
+ ///
+ public Patient? GetPatient(int id) => _context.Patients.Find(id);
+
+ ///
+ /// Returns all patients from the database as a read-only collection.
+ ///
+ public IReadOnlyCollection GetAllPatients() =>
+ _context.Patients.AsNoTracking().ToList();
+
+ ///
+ /// Adds a new patient to the database. Returns true on success.
+ ///
+ public bool AddPatient(Patient patient)
+ {
+ if (_context.Patients.Any(p => p.Id == patient.Id))
+ {
+ return false;
+ }
+
+ _context.Patients.Add(patient);
+ _context.SaveChanges();
+ return true;
+ }
+
+ ///
+ /// Updates an existing patient. Returns true if the patient existed and was updated.
+ ///
+ public bool UpdatePatient(Patient patient)
+ {
+ if (!_context.Patients.Any(p => p.Id == patient.Id))
+ {
+ return false;
+ }
+
+ _context.Patients.Update(patient);
+ _context.SaveChanges();
+ return true;
+ }
+
+ ///
+ /// Removes a patient by identifier. Returns true if removed.
+ ///
+ public bool RemovePatient(int id)
+ {
+ var entity = _context.Patients.Find(id);
+ if (entity is null)
+ {
+ return false;
+ }
+
+ _context.Patients.Remove(entity);
+ _context.SaveChanges();
+ return true;
+ }
+
+ ///
+ /// Returns total number of patients in the database.
+ ///
+ public int PatientCount() => _context.Patients.Count();
+}
+
+
diff --git a/Clinic/Clinic.DataBase/EntityFramework/EfSpecializationRepository.cs b/Clinic/Clinic.DataBase/EntityFramework/EfSpecializationRepository.cs
new file mode 100644
index 000000000..fa2b69d8d
--- /dev/null
+++ b/Clinic/Clinic.DataBase/EntityFramework/EfSpecializationRepository.cs
@@ -0,0 +1,90 @@
+using Clinic.Models.Entities;
+using Clinic.Application.Ports;
+using Microsoft.EntityFrameworkCore;
+
+namespace Clinic.DataBase.EntityFramework;
+
+///
+/// Entity Framework implementation of that manages
+/// specialization entities using .
+///
+public sealed class EfSpecializationRepository : ISpecializationRepository
+{
+ private readonly ClinicDbContext _context;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The database context used for data access.
+ public EfSpecializationRepository(ClinicDbContext context)
+ {
+ _context = context;
+ }
+
+ ///
+ /// Returns all specializations as a read-only collection.
+ ///
+ public IReadOnlyCollection GetAllSpecializations() =>
+ _context.Specializations.AsNoTracking().ToList();
+
+ ///
+ /// Adds a new specialization. Returns true on success.
+ ///
+ public bool AddSpecialization(Specialization specialization)
+ {
+ if (_context.Specializations.Any(s => s.Id == specialization.Id))
+ {
+ return false;
+ }
+
+ _context.Specializations.Add(specialization);
+ _context.SaveChanges();
+ return true;
+ }
+
+ ///
+ /// Updates a specialization by id and returns the updated entity, or null if not found.
+ ///
+ public Specialization? UpdateSpecialization(int id, Specialization specialization)
+ {
+ var existing = _context.Specializations.Find(id);
+ if (existing is null)
+ {
+ return null;
+ }
+
+ existing.Name = specialization.Name;
+ _context.SaveChanges();
+ return existing;
+ }
+
+
+ ///
+ /// Retrieves a specialization by identifier.
+ ///
+ public Specialization? GetSpecialization(int id) =>
+ _context.Specializations.Find(id);
+
+ ///
+ /// Removes a specialization by id. Returns true if removed.
+ ///
+ public bool RemoveSpecialization(int id)
+ {
+ var entity = _context.Specializations.Find(id);
+ if (entity is null)
+ {
+ return false;
+ }
+
+ _context.Specializations.Remove(entity);
+ _context.SaveChanges();
+ return true;
+ }
+
+ ///
+ /// Returns total number of specializations.
+ ///
+ public int SpecializationCount() => _context.Specializations.Count();
+}
+
+
diff --git a/Clinic/Clinic.DataBase/Migrations/20251222091310_InitialCreate.Designer.cs b/Clinic/Clinic.DataBase/Migrations/20251222091310_InitialCreate.Designer.cs
new file mode 100644
index 000000000..557d05685
--- /dev/null
+++ b/Clinic/Clinic.DataBase/Migrations/20251222091310_InitialCreate.Designer.cs
@@ -0,0 +1,235 @@
+//
+using System;
+using Clinic.DataBase;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Clinic.DataBase.Migrations
+{
+ [DbContext(typeof(ClinicDbContext))]
+ [Migration("20251222091310_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Clinic.Models.Entities.Appointment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("DateTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DoctorFullName")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)");
+
+ b.Property("DoctorId")
+ .HasColumnType("integer");
+
+ b.Property("IsReturnVisit")
+ .HasColumnType("boolean");
+
+ b.Property("PatientFullName")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)");
+
+ b.Property("PatientId")
+ .HasColumnType("integer");
+
+ b.Property("RoomNumber")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DoctorId");
+
+ b.HasIndex("PatientId");
+
+ b.ToTable("Appointments");
+ });
+
+ modelBuilder.Entity("Clinic.Models.Entities.Doctor", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BirthDate")
+ .HasColumnType("date");
+
+ b.Property("ExperienceYears")
+ .HasColumnType("integer");
+
+ b.Property("FirstName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Gender")
+ .HasColumnType("integer");
+
+ b.Property("LastName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("PassportNumber")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Patronymic")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("PhoneNumber")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Doctors");
+ });
+
+ modelBuilder.Entity("Clinic.Models.Entities.Patient", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("BirthDate")
+ .HasColumnType("date");
+
+ b.Property("BloodGroup")
+ .HasColumnType("integer");
+
+ b.Property("FirstName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Gender")
+ .HasColumnType("integer");
+
+ b.Property("LastName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("PassportNumber")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Patronymic")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("PhoneNumber")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("RhesusFactor")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("Patients");
+ });
+
+ modelBuilder.Entity("Clinic.Models.Entities.Specialization", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Specializations");
+ });
+
+ modelBuilder.Entity("DoctorSpecialization", b =>
+ {
+ b.Property("DoctorId")
+ .HasColumnType("integer");
+
+ b.Property("SpecializationId")
+ .HasColumnType("integer");
+
+ b.HasKey("DoctorId", "SpecializationId");
+
+ b.HasIndex("SpecializationId");
+
+ b.ToTable("DoctorSpecialization");
+ });
+
+ modelBuilder.Entity("Clinic.Models.Entities.Appointment", b =>
+ {
+ b.HasOne("Clinic.Models.Entities.Doctor", null)
+ .WithMany()
+ .HasForeignKey("DoctorId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("Clinic.Models.Entities.Patient", null)
+ .WithMany()
+ .HasForeignKey("PatientId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("DoctorSpecialization", b =>
+ {
+ b.HasOne("Clinic.Models.Entities.Doctor", null)
+ .WithMany()
+ .HasForeignKey("DoctorId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Clinic.Models.Entities.Specialization", null)
+ .WithMany()
+ .HasForeignKey("SpecializationId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Clinic/Clinic.DataBase/Migrations/20251222091310_InitialCreate.cs b/Clinic/Clinic.DataBase/Migrations/20251222091310_InitialCreate.cs
new file mode 100644
index 000000000..c14857f80
--- /dev/null
+++ b/Clinic/Clinic.DataBase/Migrations/20251222091310_InitialCreate.cs
@@ -0,0 +1,160 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Clinic.DataBase.Migrations
+{
+ ///
+ public partial class InitialCreate : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Doctors",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ ExperienceYears = table.Column(type: "integer", nullable: false),
+ PassportNumber = table.Column(type: "character varying(50)", maxLength: 50, nullable: false),
+ BirthDate = table.Column(type: "date", nullable: false),
+ LastName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false),
+ FirstName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false),
+ Patronymic = table.Column(type: "character varying(100)", maxLength: 100, nullable: true),
+ PhoneNumber = table.Column(type: "character varying(20)", maxLength: 20, nullable: false),
+ Gender = table.Column(type: "integer", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Doctors", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Patients",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Address = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ BloodGroup = table.Column(type: "integer", nullable: false),
+ RhesusFactor = table.Column(type: "integer", nullable: false),
+ PassportNumber = table.Column(type: "character varying(50)", maxLength: 50, nullable: false),
+ BirthDate = table.Column(type: "date", nullable: false),
+ LastName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false),
+ FirstName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false),
+ Patronymic = table.Column(type: "character varying(100)", maxLength: 100, nullable: true),
+ PhoneNumber = table.Column(type: "character varying(20)", maxLength: 20, nullable: false),
+ Gender = table.Column(type: "integer", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Patients", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Specializations",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Specializations", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Appointments",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ PatientId = table.Column(type: "integer", nullable: false),
+ PatientFullName = table.Column(type: "character varying(300)", maxLength: 300, nullable: false),
+ DoctorId = table.Column(type: "integer", nullable: false),
+ DoctorFullName = table.Column(type: "character varying(300)", maxLength: 300, nullable: false),
+ DateTime = table.Column(type: "timestamp with time zone", nullable: false),
+ RoomNumber = table.Column(type: "integer", nullable: false),
+ IsReturnVisit = table.Column(type: "boolean", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Appointments", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Appointments_Doctors_DoctorId",
+ column: x => x.DoctorId,
+ principalTable: "Doctors",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ table.ForeignKey(
+ name: "FK_Appointments_Patients_PatientId",
+ column: x => x.PatientId,
+ principalTable: "Patients",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "DoctorSpecialization",
+ columns: table => new
+ {
+ DoctorId = table.Column(type: "integer", nullable: false),
+ SpecializationId = table.Column(type: "integer", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_DoctorSpecialization", x => new { x.DoctorId, x.SpecializationId });
+ table.ForeignKey(
+ name: "FK_DoctorSpecialization_Doctors_DoctorId",
+ column: x => x.DoctorId,
+ principalTable: "Doctors",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_DoctorSpecialization_Specializations_SpecializationId",
+ column: x => x.SpecializationId,
+ principalTable: "Specializations",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Appointments_DoctorId",
+ table: "Appointments",
+ column: "DoctorId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Appointments_PatientId",
+ table: "Appointments",
+ column: "PatientId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_DoctorSpecialization_SpecializationId",
+ table: "DoctorSpecialization",
+ column: "SpecializationId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Appointments");
+
+ migrationBuilder.DropTable(
+ name: "DoctorSpecialization");
+
+ migrationBuilder.DropTable(
+ name: "Patients");
+
+ migrationBuilder.DropTable(
+ name: "Doctors");
+
+ migrationBuilder.DropTable(
+ name: "Specializations");
+ }
+ }
+}
diff --git a/Clinic/Clinic.DataBase/Migrations/20251222091340_SeedData.Designer.cs b/Clinic/Clinic.DataBase/Migrations/20251222091340_SeedData.Designer.cs
new file mode 100644
index 000000000..6181cd4b4
--- /dev/null
+++ b/Clinic/Clinic.DataBase/Migrations/20251222091340_SeedData.Designer.cs
@@ -0,0 +1,235 @@
+//
+using System;
+using Clinic.DataBase;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Clinic.DataBase.Migrations
+{
+ [DbContext(typeof(ClinicDbContext))]
+ [Migration("20251222091340_SeedData")]
+ partial class SeedData
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Clinic.Models.Entities.Appointment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("DateTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DoctorFullName")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)");
+
+ b.Property("DoctorId")
+ .HasColumnType("integer");
+
+ b.Property("IsReturnVisit")
+ .HasColumnType("boolean");
+
+ b.Property("PatientFullName")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)");
+
+ b.Property("PatientId")
+ .HasColumnType("integer");
+
+ b.Property("RoomNumber")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DoctorId");
+
+ b.HasIndex("PatientId");
+
+ b.ToTable("Appointments");
+ });
+
+ modelBuilder.Entity("Clinic.Models.Entities.Doctor", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BirthDate")
+ .HasColumnType("date");
+
+ b.Property("ExperienceYears")
+ .HasColumnType("integer");
+
+ b.Property("FirstName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Gender")
+ .HasColumnType("integer");
+
+ b.Property("LastName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("PassportNumber")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Patronymic")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("PhoneNumber")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Doctors");
+ });
+
+ modelBuilder.Entity("Clinic.Models.Entities.Patient", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("BirthDate")
+ .HasColumnType("date");
+
+ b.Property("BloodGroup")
+ .HasColumnType("integer");
+
+ b.Property("FirstName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Gender")
+ .HasColumnType("integer");
+
+ b.Property("LastName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("PassportNumber")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Patronymic")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("PhoneNumber")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("RhesusFactor")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("Patients");
+ });
+
+ modelBuilder.Entity("Clinic.Models.Entities.Specialization", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Specializations");
+ });
+
+ modelBuilder.Entity("DoctorSpecialization", b =>
+ {
+ b.Property("DoctorId")
+ .HasColumnType("integer");
+
+ b.Property("SpecializationId")
+ .HasColumnType("integer");
+
+ b.HasKey("DoctorId", "SpecializationId");
+
+ b.HasIndex("SpecializationId");
+
+ b.ToTable("DoctorSpecialization");
+ });
+
+ modelBuilder.Entity("Clinic.Models.Entities.Appointment", b =>
+ {
+ b.HasOne("Clinic.Models.Entities.Doctor", null)
+ .WithMany()
+ .HasForeignKey("DoctorId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("Clinic.Models.Entities.Patient", null)
+ .WithMany()
+ .HasForeignKey("PatientId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("DoctorSpecialization", b =>
+ {
+ b.HasOne("Clinic.Models.Entities.Doctor", null)
+ .WithMany()
+ .HasForeignKey("DoctorId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Clinic.Models.Entities.Specialization", null)
+ .WithMany()
+ .HasForeignKey("SpecializationId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Clinic/Clinic.DataBase/Migrations/20251222091340_SeedData.cs b/Clinic/Clinic.DataBase/Migrations/20251222091340_SeedData.cs
new file mode 100644
index 000000000..972142329
--- /dev/null
+++ b/Clinic/Clinic.DataBase/Migrations/20251222091340_SeedData.cs
@@ -0,0 +1,163 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Clinic.DataBase.Migrations
+{
+ ///
+ public partial class SeedData : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ // Seed Specializations
+ migrationBuilder.InsertData(
+ table: "Specializations",
+ columns: new[] { "Id", "Name" },
+ values: new object[,]
+ {
+ { 1, "Терапевт" },
+ { 2, "Хирург" },
+ { 3, "Кардиолог" },
+ { 4, "Невролог" },
+ { 5, "Офтальмолог" },
+ { 6, "Отоларинголог" },
+ { 7, "Дерматолог" },
+ { 8, "Педиатр" },
+ { 9, "Гинеколог" },
+ { 10, "Уролог" }
+ });
+
+ // Seed Doctors (10 doctors)
+ migrationBuilder.InsertData(
+ table: "Doctors",
+ columns: new[] { "Id", "PassportNumber", "BirthDate", "LastName", "FirstName", "Patronymic", "PhoneNumber", "Gender", "ExperienceYears" },
+ values: new object[,]
+ {
+ { 1, "AB1234567", new DateOnly(1975, 5, 15), "Иванов", "Иван", "Иванович", "+7-900-123-45-67", 0, 15 },
+ { 2, "CD2345678", new DateOnly(1980, 8, 22), "Петрова", "Мария", "Сергеевна", "+7-900-234-56-78", 1, 12 },
+ { 3, "EF3456789", new DateOnly(1972, 3, 10), "Сидоров", "Александр", "Петрович", "+7-900-345-67-89", 0, 20 },
+ { 4, "GH4567890", new DateOnly(1985, 11, 5), "Козлова", "Елена", "Владимировна", "+7-900-456-78-90", 1, 8 },
+ { 5, "IJ5678901", new DateOnly(1978, 7, 30), "Морозов", "Дмитрий", "Александрович", "+7-900-567-89-01", 0, 14 },
+ { 6, "KL6789012", new DateOnly(1982, 4, 18), "Волкова", "Анна", "Дмитриевна", "+7-900-678-90-12", 1, 10 },
+ { 7, "MN7890123", new DateOnly(1976, 9, 25), "Лебедев", "Сергей", "Викторович", "+7-900-789-01-23", 0, 16 },
+ { 8, "OP8901234", new DateOnly(1988, 1, 12), "Новикова", "Ольга", "Андреевна", "+7-900-890-12-34", 1, 6 },
+ { 9, "QR9012345", new DateOnly(1974, 6, 8), "Федоров", "Максим", "Сергеевич", "+7-900-901-23-45", 0, 18 },
+ { 10, "ST0123456", new DateOnly(1983, 12, 20), "Смирнова", "Татьяна", "Игоревна", "+7-900-012-34-56", 1, 11 }
+ });
+
+ // Seed Doctor-Specialization relationships
+ migrationBuilder.InsertData(
+ table: "DoctorSpecialization",
+ columns: new[] { "DoctorId", "SpecializationId" },
+ values: new object[,]
+ {
+ { 1, 1 },
+ { 1, 3 },
+ { 2, 2 },
+ { 3, 4 },
+ { 3, 1 },
+ { 4, 5 },
+ { 5, 6 },
+ { 5, 7 },
+ { 6, 8 },
+ { 7, 2 },
+ { 7, 10 },
+ { 8, 9 },
+ { 9, 3 },
+ { 9, 1 },
+ { 10, 5 },
+ { 10, 6 }
+ });
+
+ // Seed Patients (10 patients)
+ migrationBuilder.InsertData(
+ table: "Patients",
+ columns: new[] { "Id", "PassportNumber", "BirthDate", "LastName", "FirstName", "Patronymic", "PhoneNumber", "Gender", "Address", "BloodGroup", "RhesusFactor" },
+ values: new object[,]
+ {
+ { 1, "KL6789012", new DateOnly(1970, 2, 14), "Смирнов", "Алексей", "Игоревич", "+7-901-111-11-11", 0, "г. Москва, ул. Ленина, д. 10, кв. 5", 0, 0 },
+ { 2, "MN7890123", new DateOnly(1975, 6, 20), "Волкова", "Анна", "Дмитриевна", "+7-901-222-22-22", 1, "г. Москва, ул. Пушкина, д. 25, кв. 12", 1, 0 },
+ { 3, "OP8901234", new DateOnly(1972, 9, 8), "Лебедев", "Сергей", "Викторович", "+7-901-333-33-33", 0, "г. Москва, пр. Мира, д. 50, кв. 8", 2, 1 },
+ { 4, "QR9012345", new DateOnly(1988, 12, 3), "Новикова", "Ольга", "Андреевна", "+7-901-444-44-44", 1, "г. Москва, ул. Гагарина, д. 15, кв. 20", 3, 0 },
+ { 5, "ST0123456", new DateOnly(1995, 4, 17), "Федоров", "Максим", "Сергеевич", "+7-901-555-55-55", 0, "г. Москва, ул. Чехова, д. 30, кв. 15", 0, 1 },
+ { 6, "UV1234567", new DateOnly(1987, 7, 22), "Кузнецова", "Екатерина", "Александровна", "+7-901-666-66-66", 1, "г. Москва, ул. Тверская, д. 5, кв. 3", 1, 1 },
+ { 7, "WX2345678", new DateOnly(1993, 11, 30), "Попов", "Андрей", "Николаевич", "+7-901-777-77-77", 0, "г. Москва, ул. Арбат, д. 20, кв. 7", 2, 0 },
+ { 8, "YZ3456789", new DateOnly(1989, 3, 15), "Соколова", "Мария", "Владимировна", "+7-901-888-88-88", 1, "г. Москва, ул. Садовая, д. 12, кв. 9", 3, 1 },
+ { 9, "AA4567890", new DateOnly(1991, 8, 5), "Михайлов", "Игорь", "Борисович", "+7-901-999-99-99", 0, "г. Москва, ул. Невский, д. 40, кв. 11", 0, 0 },
+ { 10, "BB5678901", new DateOnly(1986, 1, 28), "Павлова", "Наталья", "Сергеевна", "+7-901-000-00-00", 1, "г. Москва, ул. Красная, д. 8, кв. 4", 1, 0 }
+ });
+
+ // Seed Appointments (15 appointments)
+ migrationBuilder.InsertData(
+ table: "Appointments",
+ columns: new[] { "Id", "PatientId", "PatientFullName", "DoctorId", "DoctorFullName", "DateTime", "RoomNumber", "IsReturnVisit" },
+ values: new object[,]
+ {
+ { 1, 1, "Смирнов Алексей Игоревич", 1, "Иванов Иван Иванович", new DateTime(2025, 12, 10, 10, 0, 0, 0, DateTimeKind.Utc), 101, false },
+ { 2, 2, "Волкова Анна Дмитриевна", 2, "Петрова Мария Сергеевна", new DateTime(2025, 12, 10, 11, 30, 0, 0, DateTimeKind.Utc), 205, false },
+ { 3, 3, "Лебедев Сергей Викторович", 3, "Сидоров Александр Петрович", new DateTime(2025, 8, 21, 9, 0, 0, 0, DateTimeKind.Utc), 302, true },
+ { 4, 4, "Новикова Ольга Андреевна", 4, "Козлова Елена Владимировна", new DateTime(2025, 12, 21, 14, 0, 0, 0, DateTimeKind.Utc), 401, false },
+ { 5, 5, "Федоров Максим Сергеевич", 5, "Морозов Дмитрий Александрович", new DateTime(2025, 12, 22, 10, 30, 0, 0, DateTimeKind.Utc), 503, false },
+ { 6, 6, "Кузнецова Екатерина Александровна", 6, "Волкова Анна Дмитриевна", new DateTime(2025, 12, 22, 15, 0, 0, 0, DateTimeKind.Utc), 201, false },
+ { 7, 7, "Попов Андрей Николаевич", 7, "Лебедев Сергей Викторович", new DateTime(2025, 5, 13, 9, 30, 0, 0, DateTimeKind.Utc), 305, true },
+ { 8, 8, "Соколова Мария Владимировна", 8, "Новикова Ольга Андреевна", new DateTime(2025, 6, 23, 11, 0, 0, 0, DateTimeKind.Utc), 402, false },
+ { 9, 9, "Михайлов Игорь Борисович", 9, "Федоров Максим Сергеевич", new DateTime(2025, 12, 24, 10, 0, 0, 0, DateTimeKind.Utc), 104, false },
+ { 10, 10, "Павлова Наталья Сергеевна", 10, "Смирнова Татьяна Игоревна", new DateTime(2025, 12, 24, 13, 30, 0, 0, DateTimeKind.Utc), 501, false },
+ { 11, 1, "Смирнов Алексей Игоревич", 2, "Петрова Мария Сергеевна", new DateTime(2025, 3, 25, 10, 0, 0, 0, DateTimeKind.Utc), 101, true },
+ { 12, 3, "Лебедев Сергей Викторович", 5, "Морозов Дмитрий Александрович", new DateTime(2025, 4, 25, 14, 0, 0, 0, DateTimeKind.Utc), 302, true },
+ { 13, 5, "Федоров Максим Сергеевич", 5, "Морозов Дмитрий Александрович", new DateTime(2025, 11, 26, 9, 0, 0, 0, DateTimeKind.Utc), 503, true },
+ { 14, 7, "Попов Андрей Николаевич", 7, "Лебедев Сергей Викторович", new DateTime(2025, 11, 26, 11, 30, 0, 0, DateTimeKind.Utc), 305, false },
+ { 15, 9, "Михайлов Игорь Борисович", 9, "Федоров Максим Сергеевич", new DateTime(2025, 11, 27, 10, 30, 0, 0, DateTimeKind.Utc), 104, true }
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ // Remove seed data in reverse order
+ migrationBuilder.DeleteData(
+ table: "Appointments",
+ keyColumn: "Id",
+ keyValues: new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 });
+
+ migrationBuilder.DeleteData(
+ table: "Patients",
+ keyColumn: "Id",
+ keyValues: new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
+
+ migrationBuilder.DeleteData(
+ table: "DoctorSpecialization",
+ keyColumns: new[] { "DoctorId", "SpecializationId" },
+ keyValues: new object[,]
+ {
+ { 1, 1 },
+ { 1, 3 },
+ { 2, 2 },
+ { 3, 4 },
+ { 3, 1 },
+ { 4, 5 },
+ { 5, 6 },
+ { 5, 7 },
+ { 6, 8 },
+ { 7, 2 },
+ { 7, 10 },
+ { 8, 9 },
+ { 9, 3 },
+ { 9, 1 },
+ { 10, 5 },
+ { 10, 6 }
+ });
+
+ migrationBuilder.DeleteData(
+ table: "Doctors",
+ keyColumn: "Id",
+ keyValues: new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
+
+ migrationBuilder.DeleteData(
+ table: "Specializations",
+ keyColumn: "Id",
+ keyValues: new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
+ }
+ }
+}
diff --git a/Clinic/Clinic.DataBase/Migrations/ClinicDbContextModelSnapshot.cs b/Clinic/Clinic.DataBase/Migrations/ClinicDbContextModelSnapshot.cs
new file mode 100644
index 000000000..603e7633d
--- /dev/null
+++ b/Clinic/Clinic.DataBase/Migrations/ClinicDbContextModelSnapshot.cs
@@ -0,0 +1,232 @@
+//
+using System;
+using Clinic.DataBase;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Clinic.DataBase.Migrations
+{
+ [DbContext(typeof(ClinicDbContext))]
+ partial class ClinicDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Clinic.Models.Entities.Appointment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("DateTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DoctorFullName")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)");
+
+ b.Property("DoctorId")
+ .HasColumnType("integer");
+
+ b.Property("IsReturnVisit")
+ .HasColumnType("boolean");
+
+ b.Property("PatientFullName")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)");
+
+ b.Property("PatientId")
+ .HasColumnType("integer");
+
+ b.Property("RoomNumber")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DoctorId");
+
+ b.HasIndex("PatientId");
+
+ b.ToTable("Appointments");
+ });
+
+ modelBuilder.Entity("Clinic.Models.Entities.Doctor", b =>
+ {
+ b.Property