-
-
Notifications
You must be signed in to change notification settings - Fork 522
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added Initial structured for CQRS with minimal APIs as slimmed versio…
…n of endpoints sample
- Loading branch information
1 parent
1f0cfff
commit 470739d
Showing
35 changed files
with
1,616 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Warehouse | ||
- simplest CQRS flow using .NET 5 Endpoints, | ||
- example of how and where to use C# Records, Nullable Reference Types, etc, | ||
- No Event Sourcing! Using Entity Framework to show that CQRS is not bounded to Event Sourcing or any type of storage, | ||
- No Aggregates! CQRS do not need DDD. Business logic can be handled in handlers. |
91 changes: 91 additions & 0 deletions
91
...e.MinimalAPI/Warehouse.Api.Tests/Products/GettingProductDetails/GetProductDetailsTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
using System; | ||
using System.Net; | ||
using System.Threading.Tasks; | ||
using Core.Api.Testing; | ||
using FluentAssertions; | ||
using Microsoft.AspNetCore.Hosting; | ||
using Warehouse.Products.GettingProductDetails; | ||
using Warehouse.Products.RegisteringProduct; | ||
using Xunit; | ||
|
||
namespace Warehouse.Api.Tests.Products.GettingProductDetails; | ||
|
||
public class GetProductDetailsFixture: ApiFixture | ||
{ | ||
protected override string ApiUrl => "/api/products"; | ||
|
||
protected override Func<IWebHostBuilder, IWebHostBuilder> SetupWebHostBuilder => | ||
whb => WarehouseTestWebHostBuilder.Configure(whb, nameof(GetProductDetailsFixture)); | ||
|
||
public ProductDetails ExistingProduct = default!; | ||
|
||
public Guid ProductId = default!; | ||
|
||
public override async Task InitializeAsync() | ||
{ | ||
var registerProduct = new RegisterProductRequest("IN11111", "ValidName", "ValidDescription"); | ||
var registerResponse = await Post(registerProduct); | ||
|
||
registerResponse.EnsureSuccessStatusCode() | ||
.StatusCode.Should().Be(HttpStatusCode.Created); | ||
|
||
ProductId = await registerResponse.GetResultFromJson<Guid>(); | ||
|
||
var (sku, name, description) = registerProduct; | ||
ExistingProduct = new ProductDetails(ProductId, sku!, name!, description); | ||
} | ||
} | ||
|
||
public class GetProductDetailsTests: IClassFixture<GetProductDetailsFixture> | ||
{ | ||
private readonly GetProductDetailsFixture fixture; | ||
|
||
public GetProductDetailsTests(GetProductDetailsFixture fixture) | ||
{ | ||
this.fixture = fixture; | ||
} | ||
|
||
[Fact] | ||
public async Task ValidRequest_With_NoParams_ShouldReturn_200() | ||
{ | ||
// Given | ||
|
||
// When | ||
var response = await fixture.Get(fixture.ProductId.ToString()); | ||
|
||
// Then | ||
response.EnsureSuccessStatusCode() | ||
.StatusCode.Should().Be(HttpStatusCode.OK); | ||
|
||
var product = await response.GetResultFromJson<ProductDetails>(); | ||
product.Should().NotBeNull(); | ||
product.Should().BeEquivalentTo(fixture.ExistingProduct); | ||
} | ||
|
||
[Theory] | ||
[InlineData(12)] | ||
[InlineData("not-a-guid")] | ||
public async Task InvalidGuidId_ShouldReturn_400(object invalidId) | ||
{ | ||
// Given | ||
|
||
// When | ||
var response = await fixture.Get($"{invalidId}"); | ||
|
||
// Then | ||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest); | ||
} | ||
|
||
[Fact] | ||
public async Task NotExistingId_ShouldReturn_404() | ||
{ | ||
// Given | ||
var notExistingId = Guid.NewGuid(); | ||
|
||
// When | ||
var response = await fixture.Get($"{notExistingId}"); | ||
|
||
// Then | ||
response.StatusCode.Should().Be(HttpStatusCode.NotFound); | ||
} | ||
} |
143 changes: 143 additions & 0 deletions
143
Sample/Warehouse.MinimalAPI/Warehouse.Api.Tests/Products/GettingProducts/GetProductsTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Net; | ||
using System.Threading.Tasks; | ||
using Core.Api.Testing; | ||
using FluentAssertions; | ||
using Microsoft.AspNetCore.Hosting; | ||
using Warehouse.Products.GettingProducts; | ||
using Warehouse.Products.RegisteringProduct; | ||
using Xunit; | ||
|
||
namespace Warehouse.Api.Tests.Products.GettingProducts; | ||
|
||
public class GetProductsFixture: ApiFixture | ||
{ | ||
protected override string ApiUrl => "/api/products"; | ||
|
||
protected override Func<IWebHostBuilder, IWebHostBuilder> SetupWebHostBuilder => | ||
whb => WarehouseTestWebHostBuilder.Configure(whb, nameof(GetProductsFixture)); | ||
|
||
public IList<ProductListItem> RegisteredProducts = new List<ProductListItem>(); | ||
|
||
public override async Task InitializeAsync() | ||
{ | ||
var productsToRegister = new[] | ||
{ | ||
new RegisterProductRequest("ZX1234", "ValidName", "ValidDescription"), | ||
new RegisterProductRequest("AD5678", "OtherValidName", "OtherValidDescription"), | ||
new RegisterProductRequest("BH90210", "AnotherValid", "AnotherValidDescription") | ||
}; | ||
|
||
foreach (var registerProduct in productsToRegister) | ||
{ | ||
var registerResponse = await Post(registerProduct); | ||
registerResponse.EnsureSuccessStatusCode() | ||
.StatusCode.Should().Be(HttpStatusCode.Created); | ||
|
||
var createdId = await registerResponse.GetResultFromJson<Guid>(); | ||
|
||
var (sku, name, _) = registerProduct; | ||
RegisteredProducts.Add(new ProductListItem(createdId, sku!, name!)); | ||
} | ||
} | ||
} | ||
|
||
public class GetProductsTests: IClassFixture<GetProductsFixture> | ||
{ | ||
private readonly GetProductsFixture fixture; | ||
|
||
public GetProductsTests(GetProductsFixture fixture) | ||
{ | ||
this.fixture = fixture; | ||
} | ||
|
||
[Fact] | ||
public async Task ValidRequest_With_NoParams_ShouldReturn_200() | ||
{ | ||
// Given | ||
|
||
// When | ||
var response = await fixture.Get(); | ||
|
||
// Then | ||
response.EnsureSuccessStatusCode() | ||
.StatusCode.Should().Be(HttpStatusCode.OK); | ||
|
||
var products = await response.GetResultFromJson<IReadOnlyList<ProductListItem>>(); | ||
products.Should().NotBeEmpty(); | ||
products.Should().BeEquivalentTo(fixture.RegisteredProducts); | ||
} | ||
|
||
[Fact] | ||
public async Task ValidRequest_With_Filter_ShouldReturn_SubsetOfRecords() | ||
{ | ||
// Given | ||
var filteredRecord = fixture.RegisteredProducts.First(); | ||
var filter = fixture.RegisteredProducts.First().Sku.Substring(1); | ||
|
||
// When | ||
var response = await fixture.Get($"?filter={filter}"); | ||
|
||
// Then | ||
response.EnsureSuccessStatusCode() | ||
.StatusCode.Should().Be(HttpStatusCode.OK); | ||
|
||
var products = await response.GetResultFromJson<IReadOnlyList<ProductListItem>>(); | ||
products.Should().NotBeEmpty(); | ||
products.Should().BeEquivalentTo(new List<ProductListItem>{filteredRecord}); | ||
} | ||
|
||
|
||
|
||
[Fact] | ||
public async Task ValidRequest_With_Paging_ShouldReturn_PageOfRecords() | ||
{ | ||
// Given | ||
const int page = 2; | ||
const int pageSize = 1; | ||
var filteredRecords = fixture.RegisteredProducts | ||
.Skip(page - 1) | ||
.Take(pageSize) | ||
.ToList(); | ||
|
||
// When | ||
var response = await fixture.Get($"?page={page}&pageSize={pageSize}"); | ||
|
||
// Then | ||
response.EnsureSuccessStatusCode() | ||
.StatusCode.Should().Be(HttpStatusCode.OK); | ||
|
||
var products = await response.GetResultFromJson<IReadOnlyList<ProductListItem>>(); | ||
products.Should().NotBeEmpty(); | ||
products.Should().BeEquivalentTo(filteredRecords); | ||
} | ||
|
||
[Fact] | ||
public async Task NegativePage_ShouldReturn_400() | ||
{ | ||
// Given | ||
var pageSize = -20; | ||
|
||
// When | ||
var response = await fixture.Get($"?page={pageSize}"); | ||
|
||
// Then | ||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest); | ||
} | ||
|
||
[Theory] | ||
[InlineData(0)] | ||
[InlineData(-20)] | ||
public async Task NegativeOrZeroPageSize_ShouldReturn_400(int pageSize) | ||
{ | ||
// Given | ||
|
||
// When | ||
var response = await fixture.Get($"?page={pageSize}"); | ||
|
||
// Then | ||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest); | ||
} | ||
} |
94 changes: 94 additions & 0 deletions
94
...ehouse.MinimalAPI/Warehouse.Api.Tests/Products/RegisteringProduct/RegisterProductTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
using System; | ||
using System.Net; | ||
using System.Threading.Tasks; | ||
using Core.Api.Testing; | ||
using FluentAssertions; | ||
using Microsoft.AspNetCore.Hosting; | ||
using Warehouse.Products.RegisteringProduct; | ||
using Xunit; | ||
|
||
namespace Warehouse.Api.Tests.Products.RegisteringProduct; | ||
|
||
public class RegisteringProductTests | ||
{ | ||
public class RegisterProductFixture: ApiFixture | ||
{ | ||
protected override string ApiUrl => "/api/products"; | ||
|
||
protected override Func<IWebHostBuilder, IWebHostBuilder> SetupWebHostBuilder => | ||
whb => WarehouseTestWebHostBuilder.Configure(whb, nameof(RegisterProductFixture)); | ||
} | ||
|
||
public class RegisterProductTests: IClassFixture<RegisterProductFixture> | ||
{ | ||
private readonly RegisterProductFixture fixture; | ||
|
||
public RegisterProductTests(RegisterProductFixture fixture) | ||
{ | ||
this.fixture = fixture; | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(ValidRequests))] | ||
public async Task ValidRequest_ShouldReturn_201(RegisterProductRequest validRequest) | ||
{ | ||
// Given | ||
|
||
// When | ||
var response = await fixture.Post(validRequest); | ||
|
||
// Then | ||
response.EnsureSuccessStatusCode() | ||
.StatusCode.Should().Be(HttpStatusCode.Created); | ||
|
||
var createdId = await response.GetResultFromJson<Guid>(); | ||
createdId.Should().NotBeEmpty(); | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(InvalidRequests))] | ||
public async Task InvalidRequest_ShouldReturn_400(RegisterProductRequest invalidRequest) | ||
{ | ||
// Given | ||
|
||
// When | ||
var response = await fixture.Post(invalidRequest); | ||
|
||
// Then | ||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest); | ||
} | ||
|
||
[Fact] | ||
public async Task RequestForExistingSKUShouldFail_ShouldReturn_409() | ||
{ | ||
// Given | ||
var request = new RegisterProductRequest("AA2039485", ValidName, ValidDescription); | ||
|
||
var response = await fixture.Post(request); | ||
response.StatusCode.Should().Be(HttpStatusCode.Created); | ||
|
||
// When | ||
response = await fixture.Post(request); | ||
|
||
// Then | ||
response.StatusCode.Should().Be(HttpStatusCode.Conflict); | ||
} | ||
|
||
private const string ValidName = "VALID_NAME"; | ||
private static string ValidSKU => $"CC{DateTime.Now.Ticks}"; | ||
private const string ValidDescription = "VALID_DESCRIPTION"; | ||
|
||
public static TheoryData<RegisterProductRequest> ValidRequests = new() | ||
{ | ||
new RegisterProductRequest(ValidSKU, ValidName, ValidDescription), | ||
new RegisterProductRequest(ValidSKU, ValidName, null) | ||
}; | ||
|
||
public static TheoryData<RegisterProductRequest> InvalidRequests = new() | ||
{ | ||
new RegisterProductRequest(null, ValidName, ValidDescription), | ||
new RegisterProductRequest("INVALID_SKU", ValidName, ValidDescription), | ||
new RegisterProductRequest(ValidSKU, null, ValidDescription), | ||
}; | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
Sample/Warehouse.MinimalAPI/Warehouse.Api.Tests/Warehouse.Api.Tests.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net6.0</TargetFramework> | ||
<Nullable>enable</Nullable> | ||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> | ||
<VSTestLogger>trx%3bLogFileName=$(MSBuildProjectName).trx</VSTestLogger> | ||
<VSTestResultsDirectory>$(MSBuildThisFileDirectory)/bin/TestResults/$(TargetFramework)</VSTestResultsDirectory> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="FluentAssertions" Version="6.2.0" /> | ||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> | ||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.2.3" /> | ||
<PackageReference Include="xunit" Version="2.4.1" /> | ||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> | ||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
<PrivateAssets>all</PrivateAssets> | ||
</PackageReference> | ||
</ItemGroup> | ||
|
||
|
||
<ItemGroup> | ||
<FrameworkReference Include="Microsoft.AspNetCore.App" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\..\Core.Api.Testing\Core.Api.Testing.csproj" /> | ||
<ProjectReference Include="..\Warehouse.Api\Warehouse.Api.csproj" /> | ||
<ProjectReference Include="..\Warehouse\Warehouse.csproj" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<Content Include="appsettings.Development.json"> | ||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile> | ||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> | ||
</Content> | ||
<Content Include="appsettings.json"> | ||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile> | ||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> | ||
</Content> | ||
</ItemGroup> | ||
|
||
</Project> |
Oops, something went wrong.