Compare commits
4
Commits
2b05c26eb3
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
165511b57d | ||
|
|
22451db209 | ||
|
|
c76e42d89d | ||
|
|
68da43a673 |
@@ -0,0 +1,2 @@
|
||||
bin/
|
||||
obj/
|
||||
Vendored
+33
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+41
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using example_csharp_app.Interfaces;
|
||||
using example_csharp_app.Models.Mapper;
|
||||
using example_csharp_app.Models.Request;
|
||||
using example_csharp_app.Models.Response;
|
||||
|
||||
namespace example_csharp_app.Aggregators;
|
||||
|
||||
public class BookAggregator : IBookAggregator
|
||||
{
|
||||
private readonly IBookService _bookService;
|
||||
|
||||
public BookAggregator(IBookService bookService)
|
||||
{
|
||||
_bookService = bookService;
|
||||
}
|
||||
|
||||
public async Task<BookOperationResponse> AddBookAsync(BookCreateRequest request)
|
||||
{
|
||||
var result = await _bookService.AddBookAsync(request.MapToModel());
|
||||
return new BookOperationResponse()
|
||||
{
|
||||
IsSuccess = result > 0
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<BookSingleResponse> GetBookAsync(Guid bookId)
|
||||
{
|
||||
var book = await _bookService.GetBookAsync(bookId);
|
||||
return new BookSingleResponse()
|
||||
{
|
||||
Book = book
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<BookListResponse> GetBookListAsync(BookFilterRequest request)
|
||||
{
|
||||
// should make separate filter object, but feel lazy
|
||||
var bookList = await _bookService.GetBooksAsync(request);
|
||||
return new BookListResponse()
|
||||
{
|
||||
Books = bookList
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<BookOperationResponse> RemoveBookAsync(Guid bookId)
|
||||
{
|
||||
var result = await _bookService.RemoveBookAsync(bookId);
|
||||
return new BookOperationResponse()
|
||||
{
|
||||
IsSuccess = result > 0
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<BookOperationResponse> UpdateBookAsync(BookUpdateRequest request)
|
||||
{
|
||||
var result = await _bookService.UpdateBookAsync(request.Id, request.MapToModel());
|
||||
return new BookOperationResponse()
|
||||
{
|
||||
IsSuccess = result > 0
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using example_csharp_app.Interfaces;
|
||||
using example_csharp_app.Models.Request;
|
||||
using example_csharp_app.Models.Response;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
|
||||
namespace example_csharp_app.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class BookController : ControllerBase
|
||||
{
|
||||
private readonly IBookAggregator _bookAggregator;
|
||||
|
||||
public BookController(IBookAggregator bookAggregator)
|
||||
{
|
||||
_bookAggregator = bookAggregator;
|
||||
}
|
||||
|
||||
[SwaggerResponse(200, "Получить список книг", typeof(BookListResponse))]
|
||||
[HttpGet()]
|
||||
public async Task<IActionResult> GetList([FromQuery]BookFilterRequest request)
|
||||
{
|
||||
var bookList = await _bookAggregator.GetBookListAsync(request);
|
||||
return Ok(bookList);
|
||||
}
|
||||
|
||||
[SwaggerResponse(200, "Получить список книг", typeof(BookSingleResponse))]
|
||||
[HttpGet("{bookId}")]
|
||||
public async Task<IActionResult> GetBook([FromRoute] Guid bookId)
|
||||
{
|
||||
var result = await _bookAggregator.GetBookAsync(bookId);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[SwaggerResponse(200, "Добавить книгу", typeof(BookOperationResponse))]
|
||||
[HttpPost()]
|
||||
public async Task<IActionResult> AddBook([FromBody]BookCreateRequest request)
|
||||
{
|
||||
var result = await _bookAggregator.AddBookAsync(request);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[SwaggerResponse(200, "Обновить книгу", typeof(BookOperationResponse))]
|
||||
[HttpPut()]
|
||||
public async Task<IActionResult> UpdateBook([FromBody]BookUpdateRequest request)
|
||||
{
|
||||
var result = await _bookAggregator.UpdateBookAsync(request);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[SwaggerResponse(200, "Удалить книгу", typeof(BookOperationResponse))]
|
||||
[HttpDelete("{bookId}")]
|
||||
public async Task<IActionResult> RemoveBook([FromRoute]Guid bookId)
|
||||
{
|
||||
var result = await _bookAggregator.RemoveBookAsync(bookId);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using example_csharp_app.Dto;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
|
||||
namespace example_app.Database.Connector;
|
||||
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<BookDto> Books { set; get; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
builder.Entity<BookDto>()
|
||||
.ToTable("Books")
|
||||
.HasKey(x => x.Id);
|
||||
|
||||
builder.Entity<BookDto>()
|
||||
.Property(x => x.Author)
|
||||
.IsRequired();
|
||||
builder.Entity<BookDto>()
|
||||
.Property(x => x.Title)
|
||||
.IsRequired();
|
||||
builder.Entity<BookDto>()
|
||||
.Property(x => x.YearPublished)
|
||||
.IsRequired();
|
||||
builder.Entity<BookDto>()
|
||||
.Property(x => x.BookIndex)
|
||||
.IsRequired();
|
||||
|
||||
builder.Entity<BookDto>()
|
||||
.Property(x => x.BookIndex)
|
||||
.HasColumnType("xml");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace example_csharp_app.Dto;
|
||||
|
||||
public class BookDto
|
||||
{
|
||||
// Для описания модели использовал fluent api в AppDbContext
|
||||
public Guid Id { set; get; }
|
||||
public string Title { set; get; } = string.Empty;
|
||||
public string Author { set; get; } = string.Empty;
|
||||
public DateTime YearPublished { set; get; }
|
||||
public string BookIndex { set; get; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace example_csharp_app;
|
||||
|
||||
public class ErrorLogMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public ErrorLogMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext httpContext, IConfiguration configuration)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(httpContext);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// todo: logme
|
||||
Console.WriteLine(ex.Message);
|
||||
httpContext.Response.StatusCode = 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Windows.Markup;
|
||||
using System.Xml;
|
||||
|
||||
namespace example_csharp_app.Filters;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
|
||||
public class XmlAttribute : ValidationAttribute
|
||||
{
|
||||
|
||||
public XmlAttribute(): base("Passed string is not XML")
|
||||
{}
|
||||
|
||||
/// <summary>
|
||||
/// Override of <see cref="ValidationAttribute.IsValid(object)" />
|
||||
/// </summary>
|
||||
/// <param name="value">The value to test</param>
|
||||
/// <returns>
|
||||
/// Returns <see langword="false" /> if the <paramref name="value" /> is null or an empty string.
|
||||
/// If <see cref="AllowEmptyStrings" /> then <see langword="true" /> is returned for empty strings.
|
||||
/// </returns>
|
||||
public override bool IsValid(object? value)
|
||||
{
|
||||
if (value is null || value is not string)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string possiblyXml = ((string)value).Trim();
|
||||
if (!possiblyXml.StartsWith('<') && !possiblyXml.EndsWith('>'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
XmlDocument xml = new XmlDocument();
|
||||
try
|
||||
{
|
||||
xml.LoadXml(possiblyXml);
|
||||
}
|
||||
catch (XmlException e)
|
||||
{
|
||||
// logme?
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using example_csharp_app.Models.Request;
|
||||
using example_csharp_app.Models.Response;
|
||||
|
||||
namespace example_csharp_app.Interfaces;
|
||||
|
||||
public interface IBookAggregator
|
||||
{
|
||||
Task<BookListResponse> GetBookListAsync(BookFilterRequest request);
|
||||
Task<BookSingleResponse> GetBookAsync(Guid bookId);
|
||||
Task<BookOperationResponse> UpdateBookAsync(BookUpdateRequest request);
|
||||
Task<BookOperationResponse> AddBookAsync(BookCreateRequest request);
|
||||
Task<BookOperationResponse> RemoveBookAsync(Guid bookId);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using example_csharp_app.Dto;
|
||||
using example_csharp_app.Models.Request;
|
||||
|
||||
namespace example_csharp_app.Interfaces;
|
||||
|
||||
public interface IBookRepository
|
||||
{
|
||||
Task<int> AddBookAsync(BookDto dto);
|
||||
Task<int> RemoveBookAsync(BookDto dto);
|
||||
Task<BookDto?> GetBookAsync(Guid bookId, bool enableEdit = true);
|
||||
Task<IReadOnlyCollection<BookDto>> GetBookListAsync(BookFilterRequest request);
|
||||
Task<int> UpdateBookAsync(BookDto dto);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using example_csharp_app.Models.Model;
|
||||
using example_csharp_app.Models.Request;
|
||||
|
||||
namespace example_csharp_app.Interfaces;
|
||||
|
||||
public interface IBookService
|
||||
{
|
||||
Task<IReadOnlyCollection<BookModel>> GetBooksAsync(BookFilterRequest request);
|
||||
Task<BookModel?> GetBookAsync(Guid bookId);
|
||||
Task<int> UpdateBookAsync(Guid bookId, BookUpdateModel model);
|
||||
Task<int> AddBookAsync(BookCreateModel model);
|
||||
Task<int> RemoveBookAsync(Guid bookId);
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using example_app.Database.Connector;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace example_csharp_app.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260925040202_init")]
|
||||
partial class init
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.12")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("example_csharp_app.Dto.BookDto", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Author")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("BookIndex")
|
||||
.IsRequired()
|
||||
.HasColumnType("xml");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("YearPublished")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Books", (string)null);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace example_csharp_app.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class init : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Books",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Title = table.Column<string>(type: "text", nullable: false),
|
||||
Author = table.Column<string>(type: "text", nullable: false),
|
||||
YearPublished = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
BookIndex = table.Column<string>(type: "xml", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Books", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Books");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using example_app.Database.Connector;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace example_csharp_app.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
partial class AppDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.12")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("example_csharp_app.Dto.BookDto", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Author")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("BookIndex")
|
||||
.IsRequired()
|
||||
.HasColumnType("xml");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("YearPublished")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Books", (string)null);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using example_csharp_app.Dto;
|
||||
using example_csharp_app.Models.Model;
|
||||
|
||||
namespace example_csharp_app.Models.Mapper;
|
||||
|
||||
public static class BookMapper
|
||||
{
|
||||
public static BookModel MapToModel(this BookDto dto)
|
||||
{
|
||||
return new BookModel()
|
||||
{
|
||||
Author = dto.Author,
|
||||
BookIndex = dto.BookIndex,
|
||||
Id = dto.Id,
|
||||
Title = dto.Title,
|
||||
YearPublished = dto.YearPublished
|
||||
};
|
||||
}
|
||||
|
||||
public static BookDto MapToDto(this BookCreateModel model)
|
||||
{
|
||||
return new BookDto()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Author = model.Author,
|
||||
BookIndex = model.BookIndex,
|
||||
Title = model.Title,
|
||||
YearPublished = model.YearPublished
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using example_csharp_app.Models.Model;
|
||||
using example_csharp_app.Models.Request;
|
||||
|
||||
namespace example_csharp_app.Models.Mapper;
|
||||
|
||||
public static class RequestMapper
|
||||
{
|
||||
public static BookCreateModel MapToModel(this BookCreateRequest request)
|
||||
{
|
||||
return new BookCreateModel()
|
||||
{
|
||||
Author = request.Author,
|
||||
BookIndex = request.BookIndex.Trim(),
|
||||
Title = request.Title,
|
||||
YearPublished = request.YearPublished.Date.ToUniversalTime()
|
||||
};
|
||||
}
|
||||
|
||||
public static BookUpdateModel MapToModel(this BookUpdateRequest request)
|
||||
{
|
||||
return new BookUpdateModel()
|
||||
{
|
||||
Author = request.Author,
|
||||
BookIndex = request.BookIndex.Trim(),
|
||||
Title = request.Title,
|
||||
YearPublished = request.YearPublished
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace example_csharp_app.Models.Model;
|
||||
|
||||
public class BookCreateModel
|
||||
{
|
||||
public string Title { set; get; } = string.Empty;
|
||||
public string Author { set; get; } = string.Empty;
|
||||
public DateTime YearPublished { set; get; }
|
||||
public string BookIndex { set; get; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace example_csharp_app.Models.Model;
|
||||
|
||||
public class BookModel
|
||||
{
|
||||
public Guid Id { set; get; }
|
||||
public string Title { set; get; } = string.Empty;
|
||||
public string Author { set; get; } = string.Empty;
|
||||
public DateTime YearPublished { set; get; }
|
||||
public string BookIndex { set; get; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace example_csharp_app.Models.Model;
|
||||
|
||||
public class BookUpdateModel
|
||||
{
|
||||
public string Title { set; get; } = string.Empty;
|
||||
public string Author { set; get; } = string.Empty;
|
||||
public DateTime YearPublished { set; get; }
|
||||
public string BookIndex { set; get; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using example_csharp_app.Filters;
|
||||
|
||||
namespace example_csharp_app.Models.Request;
|
||||
|
||||
public class BookCreateRequest
|
||||
{
|
||||
[Required]
|
||||
public string Title { set; get; } = string.Empty;
|
||||
[Required]
|
||||
public string Author { set; get; } = string.Empty;
|
||||
[Required]
|
||||
public DateTime YearPublished { set; get; }
|
||||
[Required, Xml]
|
||||
public string BookIndex { set; get; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace example_csharp_app.Models.Request;
|
||||
|
||||
public class BookFilterRequest
|
||||
{
|
||||
public string? Author { set; get; }
|
||||
public string? Index { set; get; }
|
||||
public string? Title { set; get; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using example_csharp_app.Filters;
|
||||
|
||||
namespace example_csharp_app.Models.Request;
|
||||
|
||||
public class BookUpdateRequest
|
||||
{
|
||||
[Required]
|
||||
public Guid Id { set; get; } = Guid.Empty;
|
||||
[Required]
|
||||
public string Title { set; get; } = string.Empty;
|
||||
[Required]
|
||||
public string Author { set; get; } = string.Empty;
|
||||
[Required]
|
||||
public DateTime YearPublished { set; get; }
|
||||
[Required, Xml]
|
||||
public string BookIndex { set; get; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using example_csharp_app.Models.Model;
|
||||
|
||||
namespace example_csharp_app.Models.Response;
|
||||
|
||||
public class BookListResponse
|
||||
{
|
||||
public IReadOnlyCollection<BookModel> Books { set; get; } = [];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System;
|
||||
|
||||
namespace example_csharp_app.Models.Response;
|
||||
|
||||
public class BookOperationResponse
|
||||
{
|
||||
public bool IsSuccess { set; get; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using example_csharp_app.Models.Model;
|
||||
|
||||
namespace example_csharp_app.Models.Response;
|
||||
|
||||
public class BookSingleResponse
|
||||
{
|
||||
public BookModel? Book { set; get; }
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
|
||||
using example_app.Database.Connector;
|
||||
using example_csharp_app;
|
||||
using example_csharp_app.Aggregators;
|
||||
using example_csharp_app.Interfaces;
|
||||
using example_csharp_app.Repositories;
|
||||
using example_csharp_app.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
|
||||
namespace Program
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
private static void SetupDi(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddScoped<IBookAggregator, BookAggregator>();
|
||||
services.AddScoped<IBookService, BookService>();
|
||||
services.AddScoped<IBookRepository, BookRepository>();
|
||||
services.AddDbContext<AppDbContext>(
|
||||
(provider, builder) =>
|
||||
{
|
||||
//var settings = provider.GetRequiredService<IOptions<DbSettings>>(); // эта версия лучше
|
||||
var connectionString = configuration.GetValue<string>("ConnectionStrings:DefaultConnection");
|
||||
builder.UseNpgsql(connectionString);
|
||||
},
|
||||
ServiceLifetime.Transient
|
||||
);
|
||||
}
|
||||
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var configurationBuilder = (new ConfigurationBuilder())
|
||||
.SetBasePath(Environment.CurrentDirectory)
|
||||
.AddJsonFile("appsettings.json", false, true)
|
||||
.AddJsonFile("appsettings.{environmentName}", true, true)
|
||||
.AddEnvironmentVariables()
|
||||
.AddCommandLine(args);
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var configuration = configurationBuilder.Build();
|
||||
|
||||
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();
|
||||
app.UseSwaggerUI();
|
||||
//}
|
||||
app.MapControllers();
|
||||
app.UseMiddleware<ErrorLogMiddleware>();
|
||||
//app.UseHttpsRedirection();
|
||||
|
||||
app.Run();
|
||||
|
||||
} catch (Exception)
|
||||
{
|
||||
// log
|
||||
}
|
||||
finally
|
||||
{
|
||||
// flush log
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5222",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7189;http://localhost:5222",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
$$
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using example_app.Database.Connector;
|
||||
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;
|
||||
|
||||
public class BookRepository : IBookRepository
|
||||
{
|
||||
private readonly AppDbContext _dbContext;
|
||||
|
||||
public BookRepository(AppDbContext dbContext)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
}
|
||||
|
||||
public async Task<int> AddBookAsync(BookDto dto)
|
||||
{
|
||||
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<BookDto?> GetBookAsync(Guid bookId, bool enableEdit = true)
|
||||
{
|
||||
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<IReadOnlyCollection<BookDto>> GetBookListAsync(BookFilterRequest request)
|
||||
{
|
||||
|
||||
var books = await _dbContext.Books.FromSqlInterpolated($"SELECT * FROM \"sp_SearchBooks\" ({request.Title}, {request.Author}, {request.Index})").ToListAsync();
|
||||
return books;
|
||||
|
||||
// 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)
|
||||
{
|
||||
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<int> UpdateBookAsync(BookDto dto)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using example_csharp_app.Dto;
|
||||
using example_csharp_app.Interfaces;
|
||||
using example_csharp_app.Models.Mapper;
|
||||
using example_csharp_app.Models.Model;
|
||||
using example_csharp_app.Models.Request;
|
||||
|
||||
namespace example_csharp_app.Services;
|
||||
|
||||
public class BookService : IBookService
|
||||
{
|
||||
private readonly IBookRepository _bookRepository;
|
||||
|
||||
public BookService(IBookRepository bookRepository)
|
||||
{
|
||||
_bookRepository = bookRepository;
|
||||
}
|
||||
|
||||
public async Task<int> AddBookAsync(BookCreateModel model)
|
||||
{
|
||||
var dto = model.MapToDto();
|
||||
var result = await _bookRepository.AddBookAsync(dto);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<BookModel?> GetBookAsync(Guid bookId)
|
||||
{
|
||||
var result = await _bookRepository.GetBookAsync(bookId, enableEdit: false);
|
||||
return result?.MapToModel();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<BookModel>> GetBooksAsync(BookFilterRequest request)
|
||||
{
|
||||
var result = await _bookRepository.GetBookListAsync(request);
|
||||
return result.Select(x => x.MapToModel()).ToList();
|
||||
}
|
||||
|
||||
public async Task<int> RemoveBookAsync(Guid bookId)
|
||||
{
|
||||
var book = await GetBookForEdit(bookId);
|
||||
return await _bookRepository.RemoveBookAsync(book);
|
||||
}
|
||||
|
||||
public async Task<int> UpdateBookAsync(Guid bookId, BookUpdateModel model)
|
||||
{
|
||||
var book = await GetBookForEdit(bookId);
|
||||
|
||||
book.Author = model.Author;
|
||||
book.BookIndex = model.BookIndex;
|
||||
book.Title = model.Title;
|
||||
book.YearPublished = model.YearPublished;
|
||||
|
||||
return await _bookRepository.UpdateBookAsync(book);
|
||||
}
|
||||
|
||||
private async Task<BookDto> GetBookForEdit(Guid bookId)
|
||||
{
|
||||
var book = await _bookRepository.GetBookAsync(bookId);
|
||||
return book ?? throw new ArgumentException("Book does not exist");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>example_csharp_app</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.12" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.12" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.12" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.12" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="10.2.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.2.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.2.3" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user