Compare commits

...
3 Commits
Author SHA1 Message Date
Dmitrii Kollerov 22451db209 verbosify bad desision 2026-09-25 08:26:41 +03:00
Dmitrii Kollerov c76e42d89d add gitignore 2026-09-25 08:06:54 +03:00
Dmitrii Kollerov 68da43a673 init 2026-09-25 08:05:46 +03:00
30 changed files with 874 additions and 2 deletions
+2
View File
@@ -0,0 +1,2 @@
bin/
obj/
+63
View File
@@ -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
};
}
}
+60
View File
@@ -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);
}
}
}
+36
View File
@@ -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");
}
}
+14
View File
@@ -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;
}
+27
View File
@@ -0,0 +1,27 @@
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)
{
// todo: logme
httpContext.Response.StatusCode = 500;
}
}
}
+49
View File
@@ -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;
}
}
+14
View File
@@ -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);
}
+14
View File
@@ -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);
}
+14
View File
@@ -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
View File
@@ -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
}
}
}
+37
View File
@@ -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");
}
}
}
+53
View File
@@ -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
}
}
}
+32
View File
@@ -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
};
}
}
+30
View File
@@ -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
};
}
public static BookUpdateModel MapToModel(this BookUpdateRequest request)
{
return new BookUpdateModel()
{
Author = request.Author,
BookIndex = request.BookIndex.Trim(),
Title = request.Title,
YearPublished = request.YearPublished
};
}
}
+11
View File
@@ -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;
}
+12
View File
@@ -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;
}
+11
View File
@@ -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;
}
+17
View File
@@ -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;
}
+10
View File
@@ -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; }
}
+19
View File
@@ -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;
}
+9
View File
@@ -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; } = [];
}
+8
View File
@@ -0,0 +1,8 @@
using System;
namespace example_csharp_app.Models.Response;
public class BookOperationResponse
{
public bool IsSuccess { set; get; }
}
+9
View File
@@ -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; }
}
+83
View File
@@ -0,0 +1,83 @@
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.AddCors();
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();
SetupDi(builder.Services, configuration);
var app = builder.Build();
//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;
}
}
}
+23
View File
@@ -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"
}
}
}
}
+2 -2
View File
@@ -1,2 +1,2 @@
# example-csharp-app
# example-csharp-app
+75
View File
@@ -0,0 +1,75 @@
using System;
using example_app.Database.Connector;
using example_csharp_app.Dto;
using example_csharp_app.Interfaces;
using Microsoft.EntityFrameworkCore;
using System.Linq;
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)
{
_dbContext.Add(dto);
return await _dbContext.SaveChangesAsync();
}
public async Task<BookDto?> GetBookAsync(Guid bookId, bool enableEdit = true)
{
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)
{
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
//query = query.Where(x => x.BookIndex.Contains(request.Index));
}
return await query.ToListAsync(); // можно возвращать query как IEnumerable, но в некоторых случаях это будет приводить к исключению
// например если контекст инжектится фабрикой
}
public async Task<int> RemoveBookAsync(BookDto dto)
{
_dbContext.Books.Remove(dto);
return await _dbContext.SaveChangesAsync();
}
public async Task<int> UpdateBookAsync(BookDto dto)
{
_dbContext.Books.Update(dto);
return await _dbContext.SaveChangesAsync();
}
}
+61
View File
@@ -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();
}
}
+23
View File
@@ -0,0 +1,23 @@
<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" />
</ItemGroup>
</Project>