/
miragecatv
/
library-api-csharp
Обзор
Документация
Войти
/
miragecatv
/
library-api-csharp
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
api/Program.cs
377 строк
13 KB
Nikita P
Initial BookDB project
22 июл 2026, 21:33
22 июл 2026, 21:33
1c0f3fe
Код
Авторство
О чём код?
using System.Globalization; using System.Text; using BookDb.Api.Models; using CsvHelper; using CsvHelper.Configuration; using Microsoft.OpenApi.Models; using Npgsql; using NpgsqlTypes; var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = "BookDB API", Version = "v1", Description = "REST API домашней библиотеки BookDB" }); }); builder.Services.AddCors(options => { options.AddPolicy("Frontend", policy => policy.AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod()); }); var connectionString = builder.Configuration.GetConnectionString("BookDb") ?? throw new InvalidOperationException("ConnectionStrings:BookDb is not configured."); builder.Services.AddSingleton(NpgsqlDataSource.Create(connectionString)); var app = builder.Build(); app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "BookDB API v1"); options.RoutePrefix = "swagger"; }); app.UseCors("Frontend"); app.MapGet("/", () => Results.Redirect("/swagger")) .ExcludeFromDescription(); app.MapGet("/health", () => Results.Ok(new { status = "ok" })) .WithName("Health") .WithTags("System"); app.MapGet("/api/books", async (NpgsqlDataSource dataSource, CancellationToken cancellationToken) => { const string sql = """ SELECT edition_id, title, authors, description, original_year, publisher, series, genres, isbn, publication_year, page_count, binding_type, condition, reading_status, notes FROM book_catalog ORDER BY title, edition_id; """; var books = new List<BookCatalogItem>(); await using var command = dataSource.CreateCommand(sql); await using var reader = await command.ExecuteReaderAsync(cancellationToken); while (await reader.ReadAsync(cancellationToken)) { books.Add(new BookCatalogItem( EditionId: reader.GetInt64(0), Title: reader.GetString(1), Authors: reader.IsDBNull(2) ? null : reader.GetString(2), Description: reader.IsDBNull(3) ? null : reader.GetString(3), OriginalYear: reader.IsDBNull(4) ? null : reader.GetInt16(4), Publisher: reader.IsDBNull(5) ? null : reader.GetString(5), Series: reader.IsDBNull(6) ? null : reader.GetString(6), Genres: reader.IsDBNull(7) ? null : reader.GetString(7), Isbn: reader.IsDBNull(8) ? null : reader.GetString(8), PublicationYear: reader.IsDBNull(9) ? null : reader.GetInt16(9), PageCount: reader.IsDBNull(10) ? null : reader.GetInt32(10), BindingType: reader.IsDBNull(11) ? null : reader.GetString(11), Condition: reader.IsDBNull(12) ? null : reader.GetString(12), ReadingStatus: reader.IsDBNull(13) ? null : reader.GetString(13), Notes: reader.IsDBNull(14) ? null : reader.GetString(14) )); } return Results.Ok(books); }) .WithName("GetBooks") .WithTags("Books") .Produces<List<BookCatalogItem>>(StatusCodes.Status200OK); app.MapPost("/api/books/import", async ( IFormFile file, NpgsqlDataSource dataSource, CancellationToken cancellationToken) => { if (file.Length == 0) { return Results.BadRequest(new { error = "CSV-файл пуст." }); } if (!string.Equals(Path.GetExtension(file.FileName), ".csv", StringComparison.OrdinalIgnoreCase)) { return Results.BadRequest(new { error = "Поддерживаются только файлы с расширением .csv." }); } List<BookImportRow> rows; try { await using var stream = file.OpenReadStream(); using var reader = new StreamReader( stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), detectEncodingFromByteOrderMarks: true); var configuration = new CsvConfiguration(CultureInfo.InvariantCulture) { Delimiter = ",", HasHeaderRecord = true, HeaderValidated = null, MissingFieldFound = null, TrimOptions = TrimOptions.Trim, BadDataFound = context => throw new InvalidDataException($"Некорректные данные около строки {context.Context.Parser.Row}.") }; using var csv = new CsvReader(reader, configuration); rows = csv.GetRecords<BookImportRow>().ToList(); } catch (Exception exception) when (exception is CsvHelperException or InvalidDataException or DecoderFallbackException) { return Results.BadRequest(new { error = "Не удалось прочитать CSV-файл.", details = exception.Message }); } if (rows.Count == 0) { return Results.BadRequest(new { error = "В CSV-файле нет строк с книгами." }); } foreach (var row in rows) NormalizeRow(row); var validationErrors = ValidateRows(rows); if (validationErrors.Count > 0) { return Results.BadRequest(new BookImportResult( file.FileName, rows.Count, 0, validationErrors)); } const string insertSql = """ INSERT INTO book_catalog ( title, authors, description, original_year, publisher, series, genres, isbn, publication_year, page_count, binding_type, condition, reading_status, notes ) VALUES ( @title, @authors, @description, @original_year, @publisher, @series, @genres, @isbn, @publication_year, @page_count, @binding_type, @condition, @reading_status, @notes ); """; await using var connection = await dataSource.OpenConnectionAsync(cancellationToken); await using var transaction = await connection.BeginTransactionAsync(cancellationToken); try { for (var index = 0; index < rows.Count; index++) { var row = rows[index]; await using var command = new NpgsqlCommand(insertSql, connection, transaction); AddText(command, "title", row.Title); AddText(command, "authors", row.Authors); AddText(command, "description", row.Description); AddNullable(command, "original_year", NpgsqlDbType.Smallint, row.OriginalYear); AddText(command, "publisher", row.Publisher); AddText(command, "series", row.Series); AddText(command, "genres", row.Genres); AddText(command, "isbn", row.Isbn); AddNullable(command, "publication_year", NpgsqlDbType.Smallint, row.PublicationYear); AddNullable(command, "page_count", NpgsqlDbType.Integer, row.PageCount); AddText(command, "binding_type", row.BindingType); AddText(command, "condition", row.Condition); AddText(command, "reading_status", row.ReadingStatus); AddText(command, "notes", row.Notes); try { await command.ExecuteNonQueryAsync(cancellationToken); } catch (PostgresException exception) { await transaction.RollbackAsync(cancellationToken); return Results.Conflict(new BookImportResult( file.FileName, rows.Count, 0, [new BookImportError(index + 2, row.Title, exception.MessageText)])); } } await transaction.CommitAsync(cancellationToken); } catch { await transaction.RollbackAsync(CancellationToken.None); throw; } return Results.Ok(new BookImportResult( file.FileName, rows.Count, rows.Count, [])); }) .WithName("ImportBooks") .WithTags("Books") .Accepts<IFormFile>("multipart/form-data") .Produces<BookImportResult>(StatusCodes.Status200OK) .Produces<BookImportResult>(StatusCodes.Status400BadRequest) .Produces<BookImportResult>(StatusCodes.Status409Conflict) .DisableAntiforgery(); app.Run(); static void NormalizeRow(BookImportRow row) { row.Title = Clean(row.Title); row.Authors = Clean(row.Authors); row.Description = Clean(row.Description); row.Publisher = Clean(row.Publisher); row.Series = Clean(row.Series); row.Genres = Clean(row.Genres); row.Condition = Clean(row.Condition); row.ReadingStatus = Clean(row.ReadingStatus); row.Notes = Clean(row.Notes); row.BindingType = Clean(row.BindingType) switch { "Мягкий с клапанами" => "Мягкий", "Не определено" => null, var value => value }; if (row.PageCount is <= 0) row.PageCount = null; var isbn = Clean(row.Isbn); row.Isbn = IsIsbnPlaceholder(isbn) ? null : isbn; } static string? Clean(string? value) { return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); } static bool IsIsbnPlaceholder(string? value) { if (value is null) return true; return value.StartsWith("ISBN требует", StringComparison.OrdinalIgnoreCase) || value.StartsWith("ISBN отсутствует", StringComparison.OrdinalIgnoreCase) || value.StartsWith("Требуется сверка", StringComparison.OrdinalIgnoreCase) || value.StartsWith("ISSN ", StringComparison.OrdinalIgnoreCase); } static List<BookImportError> ValidateRows(IReadOnlyList<BookImportRow> rows) { var errors = new List<BookImportError>(); var isbnRows = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); var bindingTypes = new HashSet<string>(["Твёрдый", "Мягкий", "Интегральный", "Иной"]); var conditions = new HashSet<string>(["Новое", "Отличное", "Хорошее", "Удовлетворительное", "Плохое"]); var statuses = new HashSet<string>(["Не читал", "Читаю", "Прочитал", "Отложил"]); for (var index = 0; index < rows.Count; index++) { var row = rows[index]; var rowNumber = index + 2; if (string.IsNullOrWhiteSpace(row.Title)) errors.Add(new(rowNumber, row.Title, "Не заполнено обязательное поле title.")); if (string.IsNullOrWhiteSpace(row.Authors)) errors.Add(new(rowNumber, row.Title, "Не заполнено обязательное поле authors.")); if (row.OriginalYear is < 1 or > 2100) errors.Add(new(rowNumber, row.Title, "original_year должен быть от 1 до 2100.")); if (row.PublicationYear is < 1 or > 2100) errors.Add(new(rowNumber, row.Title, "publication_year должен быть от 1 до 2100.")); if (row.PageCount is <= 0) errors.Add(new(rowNumber, row.Title, "page_count должен быть больше нуля.")); if (!string.IsNullOrWhiteSpace(row.BindingType) && !bindingTypes.Contains(row.BindingType.Trim())) errors.Add(new(rowNumber, row.Title, $"Недопустимый binding_type: {row.BindingType}.")); if (!string.IsNullOrWhiteSpace(row.Condition) && !conditions.Contains(row.Condition.Trim())) errors.Add(new(rowNumber, row.Title, $"Недопустимый condition: {row.Condition}.")); if (!string.IsNullOrWhiteSpace(row.ReadingStatus) && !statuses.Contains(row.ReadingStatus.Trim())) errors.Add(new(rowNumber, row.Title, $"Недопустимый reading_status: {row.ReadingStatus}.")); if (!string.IsNullOrWhiteSpace(row.Isbn)) { var isbn = row.Isbn.Trim(); if (isbnRows.TryGetValue(isbn, out var firstRow)) errors.Add(new(rowNumber, row.Title, $"ISBN уже встречается в строке {firstRow}.")); else isbnRows[isbn] = rowNumber; } } return errors; } static void AddText(NpgsqlCommand command, string name, string? value) { command.Parameters.AddWithValue(name, NpgsqlDbType.Text, string.IsNullOrWhiteSpace(value) ? DBNull.Value : value.Trim()); } static void AddNullable<T>(NpgsqlCommand command, string name, NpgsqlDbType type, T? value) where T : struct { command.Parameters.AddWithValue(name, type, value.HasValue ? (object)value.Value : DBNull.Value); }