diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8846018 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/net10.0/example-csharp-app.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..57de602 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/example-csharp-app.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/example-csharp-app.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/example-csharp-app.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/ErrorLogMiddleware.cs b/ErrorLogMiddleware.cs index 142f045..6c51993 100644 --- a/ErrorLogMiddleware.cs +++ b/ErrorLogMiddleware.cs @@ -18,9 +18,10 @@ public class ErrorLogMiddleware await _next(httpContext); return; } - catch (Exception) + catch (Exception ex) { // todo: logme + Console.WriteLine(ex.Message); httpContext.Response.StatusCode = 500; } } diff --git a/Models/Mapper/RequestMapper.cs b/Models/Mapper/RequestMapper.cs index f1bf607..f8938f3 100644 --- a/Models/Mapper/RequestMapper.cs +++ b/Models/Mapper/RequestMapper.cs @@ -13,7 +13,7 @@ public static class RequestMapper Author = request.Author, BookIndex = request.BookIndex.Trim(), Title = request.Title, - YearPublished = request.YearPublished + YearPublished = request.YearPublished.Date.ToUniversalTime() }; } diff --git a/Program.cs b/Program.cs index ee9a2c6..0527a66 100644 --- a/Program.cs +++ b/Program.cs @@ -16,8 +16,6 @@ namespace Program { private static void SetupDi(IServiceCollection services, IConfiguration configuration) { - services.AddCors(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -48,11 +46,21 @@ namespace Program builder.Services.AddControllers(); builder.Services.AddSwaggerGen(); - + builder.Services.AddCors(options => + { + options.AddPolicy("AllowAll", policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); + }); + SetupDi(builder.Services, configuration); - var app = builder.Build(); + app.UseCors("AllowAll"); + app.UseCors(cors => cors.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()); //if(app.Environment.IsDevelopment()) //{ app.UseSwagger(); diff --git a/README.md b/README.md index 41d9bfd..b3dd7e9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,95 @@ # example-csharp-app +DB sql + +// pgsql + +-- Вставка +CREATE PROCEDURE "sp_InsertBook"( + Id UUID, + Title VARCHAR(255), + Author VARCHAR(255), + YearPublished TimestampTz = NULL, + BookIndex XML = NULL +) +LANGUAGE plpgsql +as $$ +BEGIN + INSERT INTO "Books" ("Id", "Title", "Author", "YearPublished", "BookIndex") + VALUES (Id, Title, Author, YearPublished, BookIndex); + +end +$$ + +-- Обновление +CREATE PROCEDURE "sp_UpdateBook"( + Id UUID, + Title VARCHAR(255), + Author VARCHAR(255), + YearPublished TimestampTz, + BookIndex xml +) +LANGUAGE plpgsql +as $$ +BEGIN + UPDATE "Books" + SET + "Title" = Title, + "Author" = Author, + "YearPublished" = YearPublished, + "BookIndex" = BookIndex + WHERE "Id" = Id; +end +$$ + +-- удаление +CREATE PROCEDURE "sp_DeleteBook"( + Id UUID +) +LANGUAGE plpgsql +as $$ +BEGIN + DELETE FROM "Books" WHERE "Id" = Id; +end +$$ + +-- получение данных +CREATE FUNCTION "sp_GetBookById"(BookId UUID) +returns table ( + "Id" UUID, + "Author" TEXT, + "Title" TEXT, + "YearPublished" TIMESTAMPTZ, + "BookIndex" XML +) +LANGUAGE plpgsql +as $$ +BEGIN + RETURN QUERY + SELECT b."Id", b."Author", b."Title", b."YearPublished", b."BookIndex" + FROM "Books" b + WHERE b."Id" = BookId; +END +$$ + +-- поиск по библиотеке +CREATE FUNCTION "sp_SearchBooks"(p_Title TEXT, p_Author TEXT, p_Index TEXT) +returns table ( + "Id" UUID, + "Author" TEXT, + "Title" TEXT, + "YearPublished" TIMESTAMPTZ, + "BookIndex" XML +) +LANGUAGE plpgsql +as $$ +BEGIN + RETURN QUERY + SELECT b."Id", b."Author", b."Title", b."YearPublished", b."BookIndex" + FROM "Books" b + WHERE + (p_Author IS NULL OR b."Author" ~* p_Author) AND + (p_Title IS NULL OR b."Title" ~* p_Title) AND + (b."BookIndex"::TEXT ILIKE CONCAT('%', p_Index, '%')); +END +$$ \ No newline at end of file diff --git a/Repositories/BookRepository.cs b/Repositories/BookRepository.cs index 797c98e..e2d3e96 100644 --- a/Repositories/BookRepository.cs +++ b/Repositories/BookRepository.cs @@ -4,6 +4,8 @@ using example_csharp_app.Dto; using example_csharp_app.Interfaces; using Microsoft.EntityFrameworkCore; using System.Linq; +using System.Data.SqlClient; +using System.Globalization; using example_csharp_app.Models.Request; namespace example_csharp_app.Repositories; @@ -19,57 +21,80 @@ public class BookRepository : IBookRepository public async Task AddBookAsync(BookDto dto) { - _dbContext.Add(dto); - return await _dbContext.SaveChangesAsync(); + var result = await _dbContext.Database.ExecuteSqlInterpolatedAsync( + $"CALL public.\"sp_InsertBook\" ({dto.Id}, {dto.Title}, {dto.Author}, {dto.YearPublished}, {dto.BookIndex}::XML)" + ); + + return 1; // Процедура в pgsql не возвращает значение - это делают функции; даже rowcount только в функции + + // _dbContext.Add(dto); + // return await _dbContext.SaveChangesAsync(); } public async Task GetBookAsync(Guid bookId, bool enableEdit = true) { - var query = _dbContext.Books.Where(x => x.Id == bookId); - if(enableEdit == false) - { - query = query.AsNoTracking(); - } + var query = _dbContext.Books.FromSqlInterpolated( + $"SELECT * FROM \"sp_GetBookById\"({bookId})" + ); + return await query.FirstOrDefaultAsync(); + + // var query = _dbContext.Books.Where(x => x.Id == bookId); + // if(enableEdit == false) + // { + // query = query.AsNoTracking(); + // } + // return await query.FirstOrDefaultAsync(); } public async Task> GetBookListAsync(BookFilterRequest request) { - IQueryable query = _dbContext.Books.AsNoTracking(); - if(request.Author != null) - { - query = query.Where(x => x.Author.Contains(request.Author)); - } - if(request.Title != null) - { - query = query.Where(x => x.Title.Contains(request.Title)); - } - if(request.Index != null) - { - var interminentQuery = await query.ToListAsync(); - return interminentQuery.Where(x => x.BookIndex.Contains(request.Index)).ToList(); - // решение плохое, но для поиска более хорошего нужно время - // возможно, потребуется изменить тип поля, либо раскидывать xml по другим таблицам. - // также возможно что в TransactSql этот момент учтен и поиск будет работать нативно - // но TransactSql у меня не раскатан, а Postgre уже стоит и крутится; так быстрее и проще в текущей ситуации + + var books = await _dbContext.Books.FromSqlInterpolated($"SELECT * FROM \"sp_SearchBooks\" ({request.Title}, {request.Author}, {request.Index})").ToListAsync(); + return books; - // xml ~~ xml - operation does not exist - //query = query.Where(x => x.BookIndex.Contains(request.Index)); - } + // IQueryable query = _dbContext.Books.AsNoTracking(); + // if(request.Author != null) + // { + // query = query.Where(x => x.Author.Contains(request.Author)); + // } + // if(request.Title != null) + // { + // query = query.Where(x => x.Title.Contains(request.Title)); + // } + // if(request.Index != null) + // { + // var interminentQuery = await query.ToListAsync(); + // return interminentQuery.Where(x => x.BookIndex.Contains(request.Index)).ToList(); + // // решение плохое - return await query.ToListAsync(); // можно возвращать query как IEnumerable, но в некоторых случаях это будет приводить к исключению - // например если контекст инжектится фабрикой + // // xml ~~ xml - operation does not exist + // //query = query.Where(x => x.BookIndex.Contains(request.Index)); + // } + + // return await query.ToListAsync(); } public async Task RemoveBookAsync(BookDto dto) { - _dbContext.Books.Remove(dto); - return await _dbContext.SaveChangesAsync(); + var result = await _dbContext.Database.ExecuteSqlInterpolatedAsync( + $"CALL \"sp_DeleteBook\"({dto.Id})" + ); + return 1; // Процедура в pgsql не возвращает значение - это делают функции; даже rowcount только в функции + + // _dbContext.Books.Remove(dto); + // return await _dbContext.SaveChangesAsync(); } public async Task UpdateBookAsync(BookDto dto) { - _dbContext.Books.Update(dto); - return await _dbContext.SaveChangesAsync(); + var result = await _dbContext.Database.ExecuteSqlInterpolatedAsync( + $"CALL \"sp_UpdateBook\"({dto.Id}, {dto.Title}, {dto.Author}, {dto.YearPublished.ToString()}, {dto.BookIndex})" + ); + + return 1; // Процедура в pgsql не возвращает значение - это делают функции; даже rowcount только в функции + + // _dbContext.Books.Update(dto); + // return await _dbContext.SaveChangesAsync(); } } diff --git a/Services/BookService.cs b/Services/BookService.cs index 51b5269..55f9ff2 100644 --- a/Services/BookService.cs +++ b/Services/BookService.cs @@ -56,6 +56,6 @@ public class BookService : IBookService private async Task GetBookForEdit(Guid bookId) { var book = await _bookRepository.GetBookAsync(bookId); - return book ?? throw new ArgumentException(); + return book ?? throw new ArgumentException("Book does not exist"); } } \ No newline at end of file diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..b96b100 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=192.168.1.40:5455;Database=aspnet-testdata;User Id={SECRET_USER};Password={SECRET_PASS}" + } +} diff --git a/example-csharp-app.csproj b/example-csharp-app.csproj index f5b99c6..fc3398f 100644 --- a/example-csharp-app.csproj +++ b/example-csharp-app.csproj @@ -18,6 +18,7 @@ + \ No newline at end of file