Compare commits

..
1 Commits
Author SHA1 Message Date
Dmitrii Kollerov 165511b57d add repository working with stored procedures 2026-09-27 13:52:01 +07:00
11 changed files with 262 additions and 40 deletions
+33
View File
@@ -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"
}
]
}
+41
View File
@@ -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"
}
]
}
+2 -1
View File
@@ -18,9 +18,10 @@ public class ErrorLogMiddleware
await _next(httpContext); await _next(httpContext);
return; return;
} }
catch (Exception) catch (Exception ex)
{ {
// todo: logme // todo: logme
Console.WriteLine(ex.Message);
httpContext.Response.StatusCode = 500; httpContext.Response.StatusCode = 500;
} }
} }
+1 -1
View File
@@ -13,7 +13,7 @@ public static class RequestMapper
Author = request.Author, Author = request.Author,
BookIndex = request.BookIndex.Trim(), BookIndex = request.BookIndex.Trim(),
Title = request.Title, Title = request.Title,
YearPublished = request.YearPublished YearPublished = request.YearPublished.Date.ToUniversalTime()
}; };
} }
+11 -3
View File
@@ -16,8 +16,6 @@ namespace Program
{ {
private static void SetupDi(IServiceCollection services, IConfiguration configuration) private static void SetupDi(IServiceCollection services, IConfiguration configuration)
{ {
services.AddCors();
services.AddScoped<IBookAggregator, BookAggregator>(); services.AddScoped<IBookAggregator, BookAggregator>();
services.AddScoped<IBookService, BookService>(); services.AddScoped<IBookService, BookService>();
services.AddScoped<IBookRepository, BookRepository>(); services.AddScoped<IBookRepository, BookRepository>();
@@ -48,11 +46,21 @@ namespace Program
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
SetupDi(builder.Services, configuration); SetupDi(builder.Services, configuration);
var app = builder.Build(); var app = builder.Build();
app.UseCors("AllowAll");
app.UseCors(cors => cors.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
//if(app.Environment.IsDevelopment()) //if(app.Environment.IsDevelopment())
//{ //{
app.UseSwagger(); app.UseSwagger();
+93
View File
@@ -1,2 +1,95 @@
# example-csharp-app # 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
$$
+58 -33
View File
@@ -4,6 +4,8 @@ using example_csharp_app.Dto;
using example_csharp_app.Interfaces; using example_csharp_app.Interfaces;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Linq; using System.Linq;
using System.Data.SqlClient;
using System.Globalization;
using example_csharp_app.Models.Request; using example_csharp_app.Models.Request;
namespace example_csharp_app.Repositories; namespace example_csharp_app.Repositories;
@@ -19,57 +21,80 @@ public class BookRepository : IBookRepository
public async Task<int> AddBookAsync(BookDto dto) public async Task<int> AddBookAsync(BookDto dto)
{ {
_dbContext.Add(dto); var result = await _dbContext.Database.ExecuteSqlInterpolatedAsync(
return await _dbContext.SaveChangesAsync(); $"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<BookDto?> GetBookAsync(Guid bookId, bool enableEdit = true) public async Task<BookDto?> GetBookAsync(Guid bookId, bool enableEdit = true)
{ {
var query = _dbContext.Books.Where(x => x.Id == bookId); var query = _dbContext.Books.FromSqlInterpolated(
if(enableEdit == false) $"SELECT * FROM \"sp_GetBookById\"({bookId})"
{ );
query = query.AsNoTracking();
}
return await query.FirstOrDefaultAsync(); 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<IReadOnlyCollection<BookDto>> GetBookListAsync(BookFilterRequest request) public async Task<IReadOnlyCollection<BookDto>> GetBookListAsync(BookFilterRequest request)
{ {
IQueryable<BookDto> 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 уже стоит и крутится; так быстрее и проще в текущей ситуации
// xml ~~ xml - operation does not exist var books = await _dbContext.Books.FromSqlInterpolated($"SELECT * FROM \"sp_SearchBooks\" ({request.Title}, {request.Author}, {request.Index})").ToListAsync();
//query = query.Where(x => x.BookIndex.Contains(request.Index)); return books;
}
return await query.ToListAsync(); // можно возвращать query как IEnumerable, но в некоторых случаях это будет приводить к исключению // IQueryable<BookDto> 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 ~~ xml - operation does not exist
// //query = query.Where(x => x.BookIndex.Contains(request.Index));
// }
// return await query.ToListAsync();
} }
public async Task<int> RemoveBookAsync(BookDto dto) public async Task<int> RemoveBookAsync(BookDto dto)
{ {
_dbContext.Books.Remove(dto); var result = await _dbContext.Database.ExecuteSqlInterpolatedAsync(
return await _dbContext.SaveChangesAsync(); $"CALL \"sp_DeleteBook\"({dto.Id})"
);
return 1; // Процедура в pgsql не возвращает значение - это делают функции; даже rowcount только в функции
// _dbContext.Books.Remove(dto);
// return await _dbContext.SaveChangesAsync();
} }
public async Task<int> UpdateBookAsync(BookDto dto) public async Task<int> UpdateBookAsync(BookDto dto)
{ {
_dbContext.Books.Update(dto); var result = await _dbContext.Database.ExecuteSqlInterpolatedAsync(
return await _dbContext.SaveChangesAsync(); $"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();
} }
} }
+1 -1
View File
@@ -56,6 +56,6 @@ public class BookService : IBookService
private async Task<BookDto> GetBookForEdit(Guid bookId) private async Task<BookDto> GetBookForEdit(Guid bookId)
{ {
var book = await _bookRepository.GetBookAsync(bookId); var book = await _bookRepository.GetBookAsync(bookId);
return book ?? throw new ArgumentException(); return book ?? throw new ArgumentException("Book does not exist");
} }
} }
+8
View File
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
+12
View File
@@ -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}"
}
}
+1
View File
@@ -18,6 +18,7 @@
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="10.2.3" /> <PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="10.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.2.3" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.2.3" /> <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.2.3" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.1.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>