Skip to content

Commit

Permalink
Added Initial structured for CQRS with minimal APIs as slimmed versio…
Browse files Browse the repository at this point in the history
…n of endpoints sample
  • Loading branch information
oskardudycz committed Apr 15, 2022
1 parent 1f0cfff commit 470739d
Show file tree
Hide file tree
Showing 35 changed files with 1,616 additions and 0 deletions.
5 changes: 5 additions & 0 deletions Sample/Warehouse.MinimalAPI/README.md
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.
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);
}
}
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);
}
}
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),
};
}
}
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>
Loading

0 comments on commit 470739d

Please sign in to comment.