Skip to content
This repository was archived by the owner on Nov 4, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions students/AlexeyMelianiuk/task_06/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Лабораторная работа 06. REST/версии и Swagger/валидация (новичок)

Цель: создать REST API с версионированием и корректной валидацией/документацией.

Задания (минимум):
1) Создайте контроллеры с атрибутом `[ApiController]`, возвращайте `ProblemDetails` для 400/404/500.
2) Добавьте версии v1 и v2 в маршрутах (`api/v{version}/...`), в v2 расширьте один DTO.
3) Реализуйте пагинацию/фильтр/сортировку стандартными параметрами (`page`, `pageSize`, `sort`, `filter`).
4) Подключите Swashbuckle: опишите DTO, добавьте примеры, отразите обе версии.
5) Включите DataAnnotations или FluentValidation.

Сдача:
- Папка src — код API.
- В readme: скрины Swagger, примеры запросов и ошибок.

Критерии — из исходных ЛР10–11, упрощены.

---

Методические материалы:
- [Лабораторная работа 06 — Методические материалы](./Лабораторная_работа_06_Методические_материалы.md)

---

## Перед Pull Request: синхронизация с основным репозиторием

Перед открытием PR синхронизируйте репозиторий форка с оригиналом:

1) Добавьте upstream (один раз):
- `git remote add upstream https://github.com/brstu/IPK-WT-P30.git`
2) Получите изменения и обновите ветку:
- `git fetch upstream`
- `git checkout main` (или ваша ветка)
- `git rebase upstream/main` (или `git merge upstream/main`)
3) Решите конфликты и отправьте изменения:
- `git push --force-with-lease` (после rebase) или `git push`

4) Обновите таблицу «Успеваемость» в корневом `README.md`:
- Добавьте себя (ФИО, ссылку на `students/<ваш_идентификатор>`),
- Отметьте выполнение этой ЛР символом ✓ в колонке “#6”.
- Пример оформления — строка тестового студента №99.

Это поможет избежать конфликтов в PR.
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using Microsoft.AspNetCore.Mvc;
using TaskAPI.Models;
using TaskAPI.Models.V1;
using System.Net;

namespace TaskAPI.Controllers.V1;

[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
public class ItemsController : ControllerBase
{
private static readonly List<ItemDto> _items = new()
{
new ItemDto { Id = 1, Name = "Item 1", Description = "Description 1", CreatedAt = DateTime.UtcNow },
new ItemDto { Id = 2, Name = "Item 2", Description = "Description 2", CreatedAt = DateTime.UtcNow },
new ItemDto { Id = 3, Name = "Test Item", Description = "Description 3", CreatedAt = DateTime.UtcNow }
};

/// <summary>
/// Получить список элементов с пагинацией и фильтрацией
/// </summary>
/// <param name="parameters">Параметры запроса</param>
/// <response code="200">Возвращает список элементов</response>
/// <response code="400">Некорректные параметры запроса</response>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<ItemDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public ActionResult<IEnumerable<ItemDto>> GetItems([FromQuery] ItemQueryParameters parameters)
{
// Применение фильтрации
var query = _items.AsQueryable();

if (!string.IsNullOrEmpty(parameters.Filter))
{
query = query.Where(i => i.Name.Contains(parameters.Filter, StringComparison.OrdinalIgnoreCase) ||
i.Description.Contains(parameters.Filter, StringComparison.OrdinalIgnoreCase));
}

// Применение сортировки
if (!string.IsNullOrEmpty(parameters.Sort))
{
query = parameters.Sort.ToLower() switch
{
"name" => parameters.SortDirection == "desc"
? query.OrderByDescending(i => i.Name)
: query.OrderBy(i => i.Name),
"id" => parameters.SortDirection == "desc"
? query.OrderByDescending(i => i.Id)
: query.OrderBy(i => i.Id),
"createdat" => parameters.SortDirection == "desc"
? query.OrderByDescending(i => i.CreatedAt)
: query.OrderBy(i => i.CreatedAt),
_ => query
};
}

// Применение пагинации
var totalCount = query.Count();
var items = query
.Skip((parameters.Page - 1) * parameters.PageSize)
.Take(parameters.PageSize)
.ToList();

// Добавление заголовков пагинации
Response.Headers.Append("X-Pagination-TotalCount", totalCount.ToString());
Response.Headers.Append("X-Pagination-PageSize", parameters.PageSize.ToString());
Response.Headers.Append("X-Pagination-CurrentPage", parameters.Page.ToString());
Response.Headers.Append("X-Pagination-TotalPages", Math.Ceiling((double)totalCount / parameters.PageSize).ToString());

return Ok(items);
}

/// <summary>
/// Получить элемент по ID
/// </summary>
/// <param name="id">ID элемента</param>
/// <response code="200">Элемент найден</response>
/// <response code="404">Элемент не найден</response>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(ItemDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public ActionResult<ItemDto> GetItem(int id)
{
var item = _items.FirstOrDefault(i => i.Id == id);

if (item == null)
{
return NotFound(new ProblemDetails
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Title = "Element not found",
Status = (int)HttpStatusCode.NotFound,
Detail = $"Element with ID {id} not found",
Instance = HttpContext.Request.Path
});
}

return Ok(item);
}

/// <summary>
/// Создать новый элемент
/// </summary>
/// <param name="item">Данные элемента</param>
/// <response code="201">Элемент создан</response>
/// <response code="400">Некорректные данные</response>
[HttpPost]
[ProducesResponseType(typeof(ItemDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public ActionResult<ItemDto> CreateItem([FromBody] ItemDto item)
{
if (!ModelState.IsValid)
{
return ValidationProblem(new ValidationProblemDetails(ModelState)
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Title = "Validation error",
Status = (int)HttpStatusCode.BadRequest,
Instance = HttpContext.Request.Path
});
}

item.Id = _items.Max(i => i.Id) + 1;
item.CreatedAt = DateTime.UtcNow;
_items.Add(item);

return CreatedAtAction(nameof(GetItem), new { id = item.Id, version = "1.0" }, item);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using Microsoft.AspNetCore.Mvc;
using TaskAPI.Models;
using TaskAPI.Models.V2;
using System.Net;

namespace TaskAPI.Controllers.V2;

[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
public class ItemsController : ControllerBase
{
private static readonly List<ItemDto> _items = new()
{
new ItemDto
{
Id = 1,
Name = "Item 1 v2",
Description = "Description 1 with extended features",
Category = "Electronics",
Price = 99.99m,
Tags = new List<string> { "new", "featured" },
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
},
new ItemDto
{
Id = 2,
Name = "Item 2 v2",
Description = "Description 2 with extended features",
Category = "Books",
Price = 29.99m,
Tags = new List<string> { "bestseller" },
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
}
};

/// <summary>
/// Получить список элементов с расширенной фильтрацией
/// </summary>
/// <param name="parameters">Параметры запроса</param>
/// <response code="200">Возвращает список элементов</response>
/// <response code="400">Некорректные параметры запроса</response>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<ItemDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public ActionResult<IEnumerable<ItemDto>> GetItems([FromQuery] ItemQueryParameters parameters)
{
var query = _items.AsQueryable();

// Базовая фильтрация
if (!string.IsNullOrEmpty(parameters.Filter))
{
query = query.Where(i => i.Name.Contains(parameters.Filter, StringComparison.OrdinalIgnoreCase) ||
i.Description.Contains(parameters.Filter, StringComparison.OrdinalIgnoreCase) ||
i.Category.Contains(parameters.Filter, StringComparison.OrdinalIgnoreCase));
}

// Фильтрация по категории (новая функция в v2)
if (!string.IsNullOrEmpty(parameters.Category))
{
query = query.Where(i => i.Category.Equals(parameters.Category, StringComparison.OrdinalIgnoreCase));
}

// Сортировка с дополнительными полями
if (!string.IsNullOrEmpty(parameters.Sort))
{
query = parameters.Sort.ToLower() switch
{
"name" => parameters.SortDirection == "desc"
? query.OrderByDescending(i => i.Name)
: query.OrderBy(i => i.Name),
"id" => parameters.SortDirection == "desc"
? query.OrderByDescending(i => i.Id)
: query.OrderBy(i => i.Id),
"createdat" => parameters.SortDirection == "desc"
? query.OrderByDescending(i => i.CreatedAt)
: query.OrderBy(i => i.CreatedAt),
"price" => parameters.SortDirection == "desc"
? query.OrderByDescending(i => i.Price)
: query.OrderBy(i => i.Price),
"category" => parameters.SortDirection == "desc"
? query.OrderByDescending(i => i.Category)
: query.OrderBy(i => i.Category),
_ => query
};
}

var totalCount = query.Count();
var items = query
.Skip((parameters.Page - 1) * parameters.PageSize)
.Take(parameters.PageSize)
.ToList();

// Расширенные заголовки пагинации
Response.Headers.Append("X-Pagination-TotalCount", totalCount.ToString());
Response.Headers.Append("X-Pagination-PageSize", parameters.PageSize.ToString());
Response.Headers.Append("X-Pagination-CurrentPage", parameters.Page.ToString());
Response.Headers.Append("X-Pagination-TotalPages", Math.Ceiling((double)totalCount / parameters.PageSize).ToString());
Response.Headers.Append("X-Pagination-HasNext", (parameters.Page * parameters.PageSize < totalCount).ToString());

return Ok(items);
}

/// <summary>
/// Получить элемент по ID с расширенной информацией
/// </summary>
/// <param name="id">ID элемента</param>
/// <response code="200">Элемент найден</response>
/// <response code="404">Элемент не найден</response>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(ItemDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public ActionResult<ItemDto> GetItem(int id)
{
var item = _items.FirstOrDefault(i => i.Id == id);

if (item == null)
{
return NotFound(new ProblemDetails
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Title = "Element not found",
Status = (int)HttpStatusCode.NotFound,
Detail = $"Element with ID {id} not found in API v2.0",
Instance = HttpContext.Request.Path,
Extensions = { ["apiVersion"] = "2.0" }
});
}

return Ok(item);
}

/// <summary>
/// Создать новый элемент с расширенными полями
/// </summary>
/// <param name="item">Данные элемента</param>
/// <response code="201">Элемент создан</response>
/// <response code="400">Некорректные данные</response>
[HttpPost]
[ProducesResponseType(typeof(ItemDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public ActionResult<ItemDto> CreateItem([FromBody] ItemDto item)
{
if (!ModelState.IsValid)
{
return ValidationProblem(new ValidationProblemDetails(ModelState)
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Title = "Validation error in API v2.0",
Status = (int)HttpStatusCode.BadRequest,
Instance = HttpContext.Request.Path,
Extensions = { ["apiVersion"] = "2.0" }
});
}

item.Id = _items.Max(i => i.Id) + 1;
item.CreatedAt = DateTime.UtcNow;
item.UpdatedAt = DateTime.UtcNow;
_items.Add(item);

return CreatedAtAction(nameof(GetItem), new { id = item.Id, version = "2.0" }, item);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Text.Json;

namespace TaskAPI.Filters;

public class SwaggerDefaultValues : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var apiDescription = context.ApiDescription;

operation.Deprecated |= apiDescription.IsDeprecated();

foreach (var responseType in context.ApiDescription.SupportedResponseTypes)
{
var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString();
var response = operation.Responses[responseKey];

foreach (var contentType in response.Content.Keys)
{
if (!responseType.ApiResponseFormats.Any(x => x.MediaType == contentType))
{
response.Content.Remove(contentType);
}
}
}

if (operation.Parameters == null)
{
return;
}

foreach (var parameter in operation.Parameters)
{
var description = apiDescription.ParameterDescriptions
.First(p => p.Name == parameter.Name);

parameter.Description ??= description.ModelMetadata?.Description;

if (parameter.Schema.Default == null && description.DefaultValue != null)
{
var json = JsonSerializer.Serialize(description.DefaultValue, description.ModelMetadata.ModelType);
parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json);
}

parameter.Required |= description.IsRequired;
}
}
}
Loading