Skip to content

Commit 5d74d11

Browse files
committed
.NET: added Swagger. Added flaky tests: non-repeatable & self-validating
1 parent dc23468 commit 5d74d11

19 files changed

Lines changed: 268 additions & 66 deletions

SockstoreNet/Application/Application.csproj

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44
<TargetFramework>net9.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
7+
<NoWarn>CS1591;IDE0290</NoWarn>
8+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
9+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
710
</PropertyGroup>
811

9-
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
10-
<NoWarn>1701;1702;IDE0270</NoWarn>
11-
</PropertyGroup>
12-
13-
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
14-
<NoWarn>1701;1702;IDE0270</NoWarn>
15-
</PropertyGroup>
12+
<ItemGroup>
13+
<PackageReference Include="ClosedXML" Version="0.105.0" />
14+
</ItemGroup>
1615

1716
<ItemGroup>
1817
<ProjectReference Include="..\Vocabulary\Vocabulary.csproj" />

SockstoreNet/Application/Ports/IProductPort.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
namespace Application.Ports;
55

6-
public interface IProductPort {
6+
public interface IProductPort
7+
{
78
Task Save(ProductAggregate product);
89
Task<ProductAggregate?> FindById(ProductId id, CancellationToken cancellationToken);
910
Task<IEnumerable<ProductAggregate>> FindAll(CancellationToken cancellationToken);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using Application.Ports;
2+
using ClosedXML.Excel;
3+
4+
namespace Application.UseCases;
5+
6+
public class CreateProductListingUseCase(IProductPort port)
7+
{
8+
public async Task<IXLWorkbook> Create()
9+
{
10+
var products = await port.FindAll(CancellationToken.None);
11+
12+
var wb = new XLWorkbook();
13+
var ws = wb.AddWorksheet("Products");
14+
15+
SetHeaderCell(ws.Cell("A1"), "Id");
16+
SetHeaderCell(ws.Cell("B1"), "Name");
17+
SetHeaderCell(ws.Cell("C1"), "Category");
18+
SetHeaderCell(ws.Cell("D1"), "Price");
19+
SetHeaderCell(ws.Cell("E1"), "Stock");
20+
21+
int rowIndex = 2;
22+
foreach (var product in products)
23+
{
24+
ws.Cell(rowIndex, "A").Value = product.Id.Value;
25+
ws.Cell(rowIndex, "B").Value = product.Name.Value;
26+
ws.Cell(rowIndex, "C").Value = product.Category.Value;
27+
ws.Cell(rowIndex, "D").Value = product.Price.Value;
28+
ws.Cell(rowIndex, "E").Value = product.Stock.Value;
29+
30+
rowIndex++;
31+
}
32+
33+
ws.Range(1, 1, rowIndex, 5).SetAutoFilter();
34+
ws.ColumnsUsed().AdjustToContents();
35+
36+
return wb;
37+
}
38+
39+
private static void SetHeaderCell(IXLCell cell, string header)
40+
{
41+
cell.Value = header;
42+
cell.Style.Font.Bold = true;
43+
cell.Style.Fill.BackgroundColor = XLColor.LightGray;
44+
}
45+
}
Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,9 @@
1-
using Application.Domain;
2-
using Application.Ports;
31
using Infrastructure.Entities;
42
using Microsoft.EntityFrameworkCore;
5-
using Vocabulary;
63

74
namespace Infrastructure.Db;
85

96
public class ProductDbContext(DbContextOptions<ProductDbContext> options) : DbContext(options)
107
{
118
public DbSet<ProductEntity> Products => Set<ProductEntity>();
12-
}
13-
14-
public class ProductRepository(ProductDbContext db) : IProductPort
15-
{
16-
public async Task Save(ProductAggregate product)
17-
{
18-
var entity = ToEntity(product);
19-
await db.Products.AddAsync(entity);
20-
await db.SaveChangesAsync();
21-
}
22-
23-
public async Task Update(ProductAggregate product)
24-
{
25-
var entity = ToEntity(product);
26-
db.Products.Update(entity);
27-
await db.SaveChangesAsync();
28-
}
29-
30-
public async Task<ProductAggregate?> FindById(ProductId id, CancellationToken cancellationToken)
31-
{
32-
var entity = await db.Products.FindAsync([id.Value], cancellationToken);
33-
return entity is null ? null : ToAggregate(entity);
34-
}
35-
36-
public async Task<IEnumerable<ProductAggregate>> FindAll(CancellationToken cancellationToken)
37-
{
38-
var products = await db.Products.ToListAsync(cancellationToken: cancellationToken);
39-
return products.Select(ToAggregate);
40-
}
41-
42-
private static ProductAggregate ToAggregate(ProductEntity e)
43-
{
44-
return new(new ProductId(e.Id), new Name(e.Name), new Category(e.Category), new Price(e.Price), new Stock(e.Stock));
45-
}
46-
47-
private static ProductEntity ToEntity(ProductAggregate a) => new()
48-
{
49-
Id = a.Id.Value,
50-
Name = a.Name.Value,
51-
Category = a.Category.Value,
52-
Price = a.Price.Value,
53-
Stock = a.Stock.Value
54-
};
559
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Application.Domain;
2+
using Application.Ports;
3+
using Infrastructure.Entities;
4+
using Microsoft.EntityFrameworkCore;
5+
using Vocabulary;
6+
7+
namespace Infrastructure.Db;
8+
9+
public class ProductRepository(ProductDbContext db) : IProductPort
10+
{
11+
public async Task Save(ProductAggregate product)
12+
{
13+
var entity = ToEntity(product);
14+
await db.Products.AddAsync(entity);
15+
await db.SaveChangesAsync();
16+
}
17+
18+
public async Task Update(ProductAggregate product)
19+
{
20+
var entity = ToEntity(product);
21+
db.Products.Update(entity);
22+
await db.SaveChangesAsync();
23+
}
24+
25+
public async Task<ProductAggregate?> FindById(ProductId id, CancellationToken cancellationToken)
26+
{
27+
var entity = await db.Products.FindAsync([id.Value], cancellationToken);
28+
return entity is null ? null : ToAggregate(entity);
29+
}
30+
31+
public async Task<IEnumerable<ProductAggregate>> FindAll(CancellationToken cancellationToken)
32+
{
33+
var products = await db.Products.ToListAsync(cancellationToken: cancellationToken);
34+
return products.Select(ToAggregate);
35+
}
36+
37+
private static ProductAggregate ToAggregate(ProductEntity e)
38+
{
39+
return new(
40+
new ProductId(e.Id),
41+
new Name(e.Name),
42+
new Category(e.Category),
43+
new Price(e.Price),
44+
new Stock(e.Stock)
45+
);
46+
}
47+
48+
private static ProductEntity ToEntity(ProductAggregate a) => new()
49+
{
50+
Id = a.Id.Value,
51+
Name = a.Name.Value,
52+
Category = a.Category.Value,
53+
Price = a.Price.Value,
54+
Stock = a.Stock.Value
55+
};
56+
}

SockstoreNet/Infrastructure/Infrastructure.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
<TargetFramework>net9.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
7+
<NoWarn>CS1591;IDE0290</NoWarn>
8+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
79
</PropertyGroup>
810

911
<ItemGroup>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Application.UseCases;
2+
using Microsoft.AspNetCore.Mvc;
3+
4+
namespace SockStoreApi.Controllers;
5+
6+
[ApiController]
7+
[Route("api/[controller]")]
8+
public class PartnerController(CreateProductListingUseCase productListing) : ControllerBase
9+
{
10+
private const string ExcelContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
11+
12+
[HttpPost(nameof(DownloadProductListing))]
13+
public async Task<FileStreamResult> DownloadProductListing()
14+
{
15+
var excel = await productListing.Create();
16+
17+
var excelStream = new MemoryStream();
18+
excel.SaveAs(excelStream);
19+
excelStream.Seek(0, SeekOrigin.Begin);
20+
return new FileStreamResult(excelStream, ExcelContentType) { FileDownloadName = "products.xlsx" };
21+
}
22+
}

SockstoreNet/SockStoreApi/Program.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
builder.Services.AddControllers();
88
builder.Services.AddApplication();
99
builder.Services.AddInfrastructure();
10-
builder.Services.AddOpenApi();
10+
builder.Services.AddSwagger();
1111

1212
var app = builder.Build();
1313

1414
// Configure the HTTP request pipeline.
15-
if (app.Environment.IsDevelopment()) {
16-
app.MapOpenApi();
17-
}
15+
// if (app.Environment.IsDevelopment())
16+
app.UseSwagger();
17+
app.UseSwaggerUI();
1818

1919
app.UseHttpsRedirection();
2020

SockstoreNet/SockStoreApi/Properties/launchSettings.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"http": {
55
"commandName": "Project",
66
"dotnetRunMessages": true,
7-
"launchBrowser": false,
7+
"launchBrowser": true,
8+
"launchUrl": "swagger",
89
"applicationUrl": "http://localhost:5119",
910
"environmentVariables": {
1011
"ASPNETCORE_ENVIRONMENT": "Development"
@@ -13,7 +14,8 @@
1314
"https": {
1415
"commandName": "Project",
1516
"dotnetRunMessages": true,
16-
"launchBrowser": false,
17+
"launchBrowser": true,
18+
"launchUrl": "swagger",
1719
"applicationUrl": "https://localhost:7230;http://localhost:5119",
1820
"environmentVariables": {
1921
"ASPNETCORE_ENVIRONMENT": "Development"

SockstoreNet/SockStoreApi/ServiceCollectionExtensions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services
1515
services.AddScoped<ICreateProduct, CreateProductUseCase>();
1616
services.AddScoped<IUpdateProduct, UpdateProductUseCase>();
1717
services.AddScoped<IUpdateStock, UpdateStockUseCase>();
18+
services.AddScoped<CreateProductListingUseCase>();
1819
services.AddScoped<IProductQuery, ProductQuery>();
1920
return services;
2021
}
@@ -28,4 +29,16 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
2829
services.AddScoped<IProductPort, ProductRepository>();
2930
return services;
3031
}
32+
33+
public static IServiceCollection AddSwagger(this IServiceCollection services)
34+
{
35+
services.AddEndpointsApiExplorer();
36+
services.AddSwaggerGen(options =>
37+
{
38+
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "SockstoreApi.xml"));
39+
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "Application.xml"));
40+
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "Vocabulary.xml"));
41+
});
42+
return services;
43+
}
3144
}

0 commit comments

Comments
 (0)