Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic CQRS with .NET 5 (endpoints, nullable reference types, records etc.) #41

Merged
merged 3 commits into from
May 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Core.Marten/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,11 @@ private static void SetStoreOptions(StoreOptions options, Config config,
{
options.Connection(config.ConnectionString);
options.AutoCreateSchemaObjects = AutoCreate.CreateOrUpdate;
options.Events.DatabaseSchemaName = config.WriteModelSchema;
options.DatabaseSchemaName = config.ReadModelSchema;

var schemaName = Environment.GetEnvironmentVariable("SchemaName");
options.Events.DatabaseSchemaName = schemaName ?? config.WriteModelSchema;
options.DatabaseSchemaName = schemaName ?? config.ReadModelSchema;

options.UseDefaultSerialization(nonPublicMembersStorage: NonPublicMembersStorage.NonPublicSetters,
enumStorage: EnumStorage.AsString);
options.Events.Daemon.Mode = config.DaemonMode;
Expand Down
2 changes: 2 additions & 0 deletions Core.Testing/ApiFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public abstract class ApiFixture: IAsyncLifetime

protected ApiFixture()
{
Environment.SetEnvironmentVariable("SchemaName", GetType().Name.ToLower());

Sut = CreateTestContext();
}

Expand Down
2 changes: 1 addition & 1 deletion Core.Testing/TestWebHostBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static IWebHostBuilder Create(Dictionary<string, string> configuration, A
configureServices ??= _ => { };

return new WebHostBuilder()
.UseEnvironment("Tests")
.UseEnvironment("Development")
.UseContentRoot(projectDir)
.UseConfiguration(new ConfigurationBuilder()
.SetBasePath(projectDir)
Expand Down
24 changes: 24 additions & 0 deletions EventSourcing.NetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tools", "Workshops\BuildYou
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solved", "Solved", "{C9011E7F-42EA-4FEE-A5BB-EFC11BBA0DB0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Warehouse", "Warehouse", "{4AC3138B-6FD1-4620-A75A-3FCACE995162}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse", "Sample\Warehouse\Warehouse\Warehouse.csproj", "{C45ACE62-41BA-49D9-956A-39B479D7A50A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse.Api", "Sample\Warehouse\Warehouse.Api\Warehouse.Api.csproj", "{76C04CB6-32C7-47EA-884A-6343BDD39644}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse.Api.Tests", "Sample\Warehouse\Warehouse.Api.Tests\Warehouse.Api.Tests.csproj", "{69B22937-CA8B-478D-97F8-4D33558B5BC9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -419,6 +427,18 @@ Global
{5C54B28C-7746-4DD0-865E-1AC0D8E8D46B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C54B28C-7746-4DD0-865E-1AC0D8E8D46B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C54B28C-7746-4DD0-865E-1AC0D8E8D46B}.Release|Any CPU.Build.0 = Release|Any CPU
{C45ACE62-41BA-49D9-956A-39B479D7A50A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C45ACE62-41BA-49D9-956A-39B479D7A50A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C45ACE62-41BA-49D9-956A-39B479D7A50A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C45ACE62-41BA-49D9-956A-39B479D7A50A}.Release|Any CPU.Build.0 = Release|Any CPU
{76C04CB6-32C7-47EA-884A-6343BDD39644}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76C04CB6-32C7-47EA-884A-6343BDD39644}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76C04CB6-32C7-47EA-884A-6343BDD39644}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76C04CB6-32C7-47EA-884A-6343BDD39644}.Release|Any CPU.Build.0 = Release|Any CPU
{69B22937-CA8B-478D-97F8-4D33558B5BC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{69B22937-CA8B-478D-97F8-4D33558B5BC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{69B22937-CA8B-478D-97F8-4D33558B5BC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{69B22937-CA8B-478D-97F8-4D33558B5BC9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -496,6 +516,10 @@ Global
{C9011E7F-42EA-4FEE-A5BB-EFC11BBA0DB0} = {94524EA9-A4BA-4684-99B8-BBE9EE85E791}
{7ACC398F-87BF-4B3E-AD61-DFB5F56D4B25} = {C9011E7F-42EA-4FEE-A5BB-EFC11BBA0DB0}
{03D0848C-7B19-4685-BA1F-59FFAF1DCEA6} = {C9011E7F-42EA-4FEE-A5BB-EFC11BBA0DB0}
{4AC3138B-6FD1-4620-A75A-3FCACE995162} = {A7186B6B-D56D-4AEF-B6B7-FAA827764C34}
{C45ACE62-41BA-49D9-956A-39B479D7A50A} = {4AC3138B-6FD1-4620-A75A-3FCACE995162}
{76C04CB6-32C7-47EA-884A-6343BDD39644} = {4AC3138B-6FD1-4620-A75A-3FCACE995162}
{69B22937-CA8B-478D-97F8-4D33558B5BC9} = {4AC3138B-6FD1-4620-A75A-3FCACE995162}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A5F55604-2FF3-43B7-B657-4F18E6E95D3B}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Core.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,144 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Core.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);
}
}
}
Loading