3 Commits

Author SHA1 Message Date
934e441e89 сделал 2025-11-18 17:17:15 +04:00
b76a22641a Сделал (ничего) 2025-11-04 23:37:47 +04:00
6160732a66 Сделал 4 лабу 2025-10-21 23:21:44 +04:00
25 changed files with 885 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
!**/.gitignore
!.git/HEAD
!.git/config
!.git/packed-refs
!.git/refs/heads/**

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>4a831111-1a8f-4dd4-b364-44bd98f36e3b</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>.</DockerfileContext>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Mvc;
using ProductMicroservice.Domain;
using ProductMicroservice.Infrastructure;
namespace ProductMicroservice.Controllers;
[ApiController]
[Route("api/[controller]")]
public class CategoriesController : ControllerBase
{
private readonly CategoryDbContext _db;
public CategoriesController(CategoryDbContext db) => _db = db;
[HttpGet]
public async Task<ActionResult<IEnumerable<Category>>> GetAll()
{
return await _db.Categories.AsNoTracking().ToListAsync();
}
[HttpGet("{id:int}")]
public async Task<ActionResult<Category>> Get(int id)
{
var cat = await _db.Categories.FindAsync(id);
if (cat is null) return NotFound();
return cat;
}
[HttpPost]
public async Task<ActionResult<Category>> Create(Category model)
{
if (string.IsNullOrWhiteSpace(model.Name))
return BadRequest("Name is required");
_db.Categories.Add(model);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(Get), new { id = model.Id }, model);
}
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(int id, Category model)
{
if (id != model.Id) return BadRequest();
if (!_db.Categories.Any(c => c.Id == id)) return NotFound();
_db.Entry(model).State = EntityState.Modified;
await _db.SaveChangesAsync();
return NoContent();
}
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id)
{
var cat = await _db.Categories.FindAsync(id);
if (cat is null) return NotFound();
_db.Categories.Remove(cat);
await _db.SaveChangesAsync();
return NoContent();
}
}

View File

@@ -0,0 +1,30 @@
# См. статью по ссылке https://aka.ms/customizecontainer, чтобы узнать как настроить контейнер отладки и как Visual Studio использует этот Dockerfile для создания образов для ускорения отладки.
# Этот этап используется при запуске из VS в быстром режиме (по умолчанию для конфигурации отладки)
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
# Этот этап используется для сборки проекта службы
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["ProductMicroservice.csproj", "."]
RUN dotnet restore "./ProductMicroservice.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "./ProductMicroservice.csproj" -c $BUILD_CONFIGURATION -o /app/build
# Этот этап используется для публикации проекта службы, который будет скопирован на последний этап
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./ProductMicroservice.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
# Этот этап используется в рабочей среде или при запуске из VS в обычном режиме (по умолчанию, когда конфигурация отладки не используется)
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ProductMicroservice.dll"]

View File

@@ -0,0 +1,7 @@
namespace ProductMicroservice.Domain;
public class Category
{
public int Id { get; set; }
public string Name { get; set; } = null!;
}

View File

@@ -0,0 +1,12 @@
using ProductMicroservice.Domain;
using System.Collections.Generic;
namespace ProductMicroservice.Infrastructure;
public class CategoryDbContext : DbContext
{
public CategoryDbContext(DbContextOptions<CategoryDbContext> options)
: base(options) { }
public DbSet<Category> Categories => Set<Category>();
}

View File

@@ -0,0 +1,6 @@
@ProductMicroservice_HostAddress = http://localhost:5186
GET {{ProductMicroservice_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36401.2
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CategoryMicroservice", "CategoryMicroservice.csproj", "{CA014D50-626B-4303-80DC-362AA95C15F6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProductService", "..\ProductService\ProductService.csproj", "{183F680C-0F79-4B0E-B1EB-7E5F3005D600}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CA014D50-626B-4303-80DC-362AA95C15F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CA014D50-626B-4303-80DC-362AA95C15F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CA014D50-626B-4303-80DC-362AA95C15F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CA014D50-626B-4303-80DC-362AA95C15F6}.Release|Any CPU.Build.0 = Release|Any CPU
{183F680C-0F79-4B0E-B1EB-7E5F3005D600}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{183F680C-0F79-4B0E-B1EB-7E5F3005D600}.Debug|Any CPU.Build.0 = Debug|Any CPU
{183F680C-0F79-4B0E-B1EB-7E5F3005D600}.Release|Any CPU.ActiveCfg = Release|Any CPU
{183F680C-0F79-4B0E-B1EB-7E5F3005D600}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2EC5F8EE-6748-4CEB-9C6F-A266F67E1176}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,34 @@
using ProductMicroservice.Infrastructure;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<CategoryDbContext>(options =>
{
var connectionString = builder.Configuration.GetConnectionString("Default");
options.UseNpgsql(connectionString);
});
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<CategoryDbContext>();
db.Database.Migrate();
}
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapControllers();
app.Run();

View File

@@ -0,0 +1,52 @@
{
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5186"
},
"https": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7112;http://localhost:5186"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Container (Dockerfile)": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
"environmentVariables": {
"ASPNETCORE_HTTPS_PORTS": "8081",
"ASPNETCORE_HTTP_PORTS": "8080"
},
"publishAllPorts": true,
"useSSL": true
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:46293",
"sslPort": 44338
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,8 @@
{
"ConnectionStrings": {
"Default": "Host=db;Port=5432;Database=products;Username=postgres;Password=1234"
},
"CategoryService": {
"BaseUrl": "http://category-api:8080/"
}
}

View File

@@ -0,0 +1,135 @@
using Microsoft.AspNetCore.Mvc;
using ProductService.Data;
using ProductService.Models;
using ProductService.Services;
namespace ProductService.Controllers;
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly ProductDbContext _db;
private readonly ICategoryClient _categoryClient;
public ProductsController(ProductDbContext db, ICategoryClient categoryClient)
{
_db = db ?? throw new ArgumentNullException(nameof(db));
_categoryClient = categoryClient ?? throw new ArgumentNullException(nameof(categoryClient));
}
[HttpGet]
public async Task<ActionResult<IEnumerable<ProductDto>>> GetAll(CancellationToken cancellationToken)
{
var products = await _db.Products
.AsNoTracking()
.ToListAsync(cancellationToken);
var categories = await _categoryClient.GetCategoriesAsync(cancellationToken);
var result = products.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Description = p.Description,
CategoryId = p.CategoryId,
CategoryName = categories.TryGetValue(p.CategoryId, out var name) ? name : "UNKNOWN",
Quantity = p.Quantity
});
return Ok(result);
}
[HttpGet("{id:int}")]
public async Task<ActionResult<ProductDto>> GetById(int id, CancellationToken cancellationToken)
{
var product = await _db.Products
.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
if (product is null)
return NotFound();
var categories = await _categoryClient.GetCategoriesAsync(cancellationToken);
var dto = new ProductDto
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
CategoryId = product.CategoryId,
CategoryName = categories.TryGetValue(product.CategoryId, out var name) ? name : "UNKNOWN",
Quantity = product.Quantity
};
return Ok(dto);
}
[HttpPost]
public async Task<ActionResult<Product>> Create([FromBody] Product model, CancellationToken cancellationToken)
{
if (!ModelState.IsValid)
return ValidationProblem(ModelState);
if (string.IsNullOrWhiteSpace(model.Name))
return BadRequest("Name is required.");
if (string.IsNullOrWhiteSpace(model.Description))
return BadRequest("Description is required.");
if (model.Quantity is < 0)
return BadRequest("Quantity cannot be negative.");
var categories = await _categoryClient.GetCategoriesAsync(cancellationToken);
if (!categories.ContainsKey(model.CategoryId))
return BadRequest($"Category with id {model.CategoryId} does not exist.");
_db.Products.Add(model);
await _db.SaveChangesAsync(cancellationToken);
return CreatedAtAction(
nameof(GetById),
new { id = model.Id },
model);
}
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(int id, [FromBody] Product model, CancellationToken cancellationToken)
{
if (id != model.Id)
return BadRequest("Route id and body id must match.");
if (!ModelState.IsValid)
return ValidationProblem(ModelState);
if (model.Quantity is < 0)
return BadRequest("Quantity cannot be negative.");
var exists = await _db.Products.AnyAsync(p => p.Id == id, cancellationToken);
if (!exists)
return NotFound();
var categories = await _categoryClient.GetCategoriesAsync(cancellationToken);
if (!categories.ContainsKey(model.CategoryId))
return BadRequest($"Category with id {model.CategoryId} does not exist.");
_db.Entry(model).State = EntityState.Modified;
await _db.SaveChangesAsync(cancellationToken);
return NoContent();
}
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
{
var product = await _db.Products.FindAsync(new object[] { id }, cancellationToken);
if (product is null)
return NotFound();
_db.Products.Remove(product);
await _db.SaveChangesAsync(cancellationToken);
return NoContent();
}
}

View File

@@ -0,0 +1,229 @@
using Microsoft.AspNetCore.Mvc;
using ProductService.Data;
using ProductService.Services;
using System.ComponentModel;
using System.Reflection.Metadata;
using static System.Net.Mime.MediaTypeNames;
namespace ProductService.Controllers;
[ApiController]
[Route("api/[controller]")]
public class ReportsController : ControllerBase
{
private readonly ProductDbContext _db;
private readonly ICategoryClient _categoryClient;
public ReportsController(ProductDbContext db, ICategoryClient categoryClient)
{
_db = db ?? throw new ArgumentNullException(nameof(db));
_categoryClient = categoryClient ?? throw new ArgumentNullException(nameof(categoryClient));
}
[HttpGet("products/in-stock/excel")]
public async Task<IActionResult> GetProductsInStockExcel(CancellationToken cancellationToken)
{
var productsInStock = await _db.Products
.AsNoTracking()
.Where(p => p.Quantity.HasValue && p.Quantity.Value > 0)
.OrderBy(p => p.Name)
.ToListAsync(cancellationToken);
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
using var package = new ExcelPackage();
var worksheet = package.Workbook.Worksheets.Add("В наличии");
worksheet.Cells[1, 1].Value = "Продукты в наличии";
worksheet.Cells[1, 1].Style.Font.Bold = true;
int row = 3;
foreach (var p in productsInStock)
{
worksheet.Cells[row, 1].Value = $"{p.Name}: {p.Description}";
row++;
}
worksheet.Cells.AutoFitColumns();
var bytes = package.GetAsByteArray();
var fileName = "ProductsInStock.xlsx";
return File(
fileContents: bytes,
contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
fileDownloadName: fileName);
}
[HttpGet("products/out-of-stock/pie-pdf")]
public async Task<IActionResult> GetProductsOutOfStockPiePdf(CancellationToken cancellationToken)
{
var productsOutOfStock = await _db.Products
.AsNoTracking()
.Where(p => !p.Quantity.HasValue || p.Quantity.Value == 0)
.ToListAsync(cancellationToken);
var categories = await _categoryClient.GetCategoriesAsync(cancellationToken);
var grouped = productsOutOfStock
.GroupBy(p => categories.TryGetValue(p.CategoryId, out var name) ? name : "UNKNOWN")
.Select(g => new
{
CategoryName = g.Key,
Count = g.Count()
})
.OrderByDescending(x => x.Count)
.ToList();
if (!grouped.Any())
{
return NoContent();
}
var document = new Document();
document.Info.Title = "Продукты, отсутствующие в наличии";
document.Info.Subject = "Круговая диаграмма по категориям";
document.Info.Author = "ProductService";
var section = document.AddSection();
section.AddParagraph("Продукты, отсутствующие в наличии", "Heading1").Format.SpaceAfter = "1cm";
var chart = section.AddChart(ChartType.Pie2D);
chart.Width = "12cm";
chart.Height = "8cm";
var series = chart.SeriesCollection.AddSeries();
var xSeries = chart.XValues.AddXSeries();
foreach (var item in grouped)
{
series.Add(item.Count);
xSeries.Add(item.CategoryName);
}
series.HasDataLabel = true;
series.DataLabel.Type = DataLabelType.Percent;
var renderer = new PdfDocumentRenderer(unicode: true)
{
Document = document
};
renderer.RenderDocument();
using var ms = new MemoryStream();
renderer.PdfDocument.Save(ms, false);
var bytes = ms.ToArray();
var fileName = "ProductsOutOfStockByCategory.pdf";
return File(
fileContents: bytes,
contentType: "application/pdf",
fileDownloadName: fileName);
}
[HttpGet("products/all/word")]
public async Task<IActionResult> GetAllProductsWord(CancellationToken cancellationToken)
{
var products = await _db.Products
.AsNoTracking()
.OrderBy(p => p.Id)
.ToListAsync(cancellationToken);
var categories = await _categoryClient.GetCategoriesAsync(cancellationToken);
using var ms = new MemoryStream();
using (var wordDoc = WordprocessingDocument.Create(ms, WordprocessingDocumentType.Document, true))
{
var mainPart = wordDoc.AddMainDocumentPart();
mainPart.Document = new Document();
var body = mainPart.Document.AppendChild(new Body());
var headingParagraph = new Paragraph(
new Run(
new Text("Список продуктов")))
{
ParagraphProperties = new ParagraphProperties(
new ParagraphStyleId { Val = "Heading1" })
};
body.AppendChild(headingParagraph);
body.AppendChild(new Paragraph(new Run(new Text(""))));
var table = new Table();
var tableProps = new TableProperties(
new TableBorders(
new TopBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4 },
new BottomBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4 },
new LeftBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4 },
new RightBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4 },
new InsideHorizontalBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4 },
new InsideVerticalBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4 }
)
);
table.AppendChild(tableProps);
var headerRow = new TableRow();
headerRow.Append(
CreateCell("ID", isHeader: true),
CreateCell("Название", isHeader: true),
CreateCell("Категория", isHeader: true),
CreateCell("Количество", isHeader: true));
table.AppendChild(headerRow);
foreach (var p in products)
{
var row = new TableRow();
var categoryName = categories.TryGetValue(p.CategoryId, out var name)
? name
: "UNKNOWN";
var quantityText = p.Quantity.HasValue
? p.Quantity.Value.ToString()
: "отсутствует";
row.Append(
CreateCell(p.Id.ToString()),
CreateCell(p.Name),
CreateCell(categoryName),
CreateCell(quantityText));
table.AppendChild(row);
}
body.AppendChild(table);
}
var bytes = ms.ToArray();
var fileName = "AllProducts.docx";
return File(
fileContents: bytes,
contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
fileDownloadName: fileName);
}
private static TableCell CreateCell(string text, bool isHeader = false)
{
var runProps = new RunProperties();
if (isHeader)
{
runProps.Bold = new Bold();
}
var run = new Run();
run.Append(runProps);
run.Append(new Text(text ?? string.Empty));
var paragraph = new Paragraph(run);
var cell = new TableCell(paragraph);
return cell;
}
}

View File

@@ -0,0 +1,34 @@
using ProductService.Models;
using System.Collections.Generic;
using System.Reflection.Emit;
namespace ProductService.Data;
public class ProductDbContext : DbContext
{
public ProductDbContext(DbContextOptions<ProductDbContext> options)
: base(options)
{
}
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
var product = modelBuilder.Entity<Product>();
product.Property(p => p.Name)
.IsRequired()
.HasMaxLength(200);
product.Property(p => p.Description)
.IsRequired()
.HasMaxLength(1000);
product.Property(p => p.CategoryId)
.IsRequired();
}
}

View File

@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
namespace ProductService.Models;
public class Product
{
public int Id { get; set; }
[Required, MaxLength(200)]
public string Name { get; set; } = null!;
[Required, MaxLength(1000)]
public string Description { get; set; } = null!;
[Required]
public int CategoryId { get; set; }
public int? Quantity { get; set; }
}

View File

@@ -0,0 +1,11 @@
namespace ProductService.Models;
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public string Description { get; set; } = null!;
public int CategoryId { get; set; }
public string CategoryName { get; set; } = null!;
public int? Quantity { get; set; }
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\ProductMicroservice</DockerfileContext>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
@ProductService_HostAddress = http://localhost:5249
GET {{ProductService_HostAddress}}/weatherforecast/
Accept: application/json
###

50
ProductService/Program.cs Normal file
View File

@@ -0,0 +1,50 @@
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<ProductDbContext>(options =>
{
var connectionString = builder.Configuration.GetConnectionString("Default");
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new InvalidOperationException("Connection string 'Default' is not configured.");
}
options.UseNpgsql(connectionString);
});
builder.Services.AddHttpClient<ICategoryClient, CategoryClient>(client =>
{
var baseUrl = builder.Configuration["CategoryService:BaseUrl"];
if (string.IsNullOrWhiteSpace(baseUrl))
{
throw new InvalidOperationException("Configuration 'CategoryService:BaseUrl' is not set.");
}
client.BaseAddress = new Uri(baseUrl);
});
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<ProductDbContext>();
db.Database.Migrate();
}
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapControllers();
app.Run();

View File

@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:11752",
"sslPort": 44385
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5249",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7100;http://localhost:5249",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,26 @@
namespace ProductService.Services;
public class CategoryClient : ICategoryClient
{
private readonly HttpClient _httpClient;
public CategoryClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
private sealed class CategoryDto
{
public int Id { get; set; }
public string Name { get; set; } = null!;
}
public async Task<Dictionary<int, string>> GetCategoriesAsync(CancellationToken cancellationToken = default)
{
var categories = await _httpClient.GetFromJsonAsync<List<CategoryDto>>(
"api/categories",
cancellationToken
) ?? new List<CategoryDto>();
return categories.ToDictionary(c => c.Id, c => c.Name);
}
}

View File

@@ -0,0 +1,6 @@
namespace ProductService.Services;
public interface ICategoryClient
{
Task<Dictionary<int, string>> GetCategoriesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,15 @@
{
"ConnectionStrings": {
"Default": "Host=db;Port=5432;Database=kop5_products;Username=postgres;Password=1234"
},
"CategoryService": {
"BaseUrl": "http://category-api:8080/"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}