Skip to content

Commit 9c348dc

Browse files
committed
Added example of basic CQRS using Endpoints, Nullable Reference Types, Records and other C# 8-9 goodies
Added tests for Registering the Product Added tests for the Warehouse example's queries Updated project and test configuration to run migrations automatically
1 parent 8881f95 commit 9c348dc

40 files changed

+1602
-9
lines changed

Core.Marten/Config.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,11 @@ private static void SetStoreOptions(StoreOptions options, Config config,
5454
{
5555
options.Connection(config.ConnectionString);
5656
options.AutoCreateSchemaObjects = AutoCreate.CreateOrUpdate;
57-
options.Events.DatabaseSchemaName = config.WriteModelSchema;
58-
options.DatabaseSchemaName = config.ReadModelSchema;
57+
58+
var schemaName = Environment.GetEnvironmentVariable("SchemaName");
59+
options.Events.DatabaseSchemaName = schemaName ?? config.WriteModelSchema;
60+
options.DatabaseSchemaName = schemaName ?? config.ReadModelSchema;
61+
5962
options.UseDefaultSerialization(nonPublicMembersStorage: NonPublicMembersStorage.NonPublicSetters,
6063
enumStorage: EnumStorage.AsString);
6164
options.Events.Daemon.Mode = config.DaemonMode;

Core.Testing/ApiFixture.cs

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public abstract class ApiFixture: IAsyncLifetime
3131

3232
protected ApiFixture()
3333
{
34+
Environment.SetEnvironmentVariable("SchemaName", GetType().Name.ToLower());
35+
3436
Sut = CreateTestContext();
3537
}
3638

Core.Testing/TestWebHostBuilder.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public static IWebHostBuilder Create(Dictionary<string, string> configuration, A
1515
configureServices ??= _ => { };
1616

1717
return new WebHostBuilder()
18-
.UseEnvironment("Tests")
18+
.UseEnvironment("Development")
1919
.UseContentRoot(projectDir)
2020
.UseConfiguration(new ConfigurationBuilder()
2121
.SetBasePath(projectDir)

EventSourcing.NetCore.sln

+24
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tools", "Workshops\BuildYou
177177
EndProject
178178
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solved", "Solved", "{C9011E7F-42EA-4FEE-A5BB-EFC11BBA0DB0}"
179179
EndProject
180+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Warehouse", "Warehouse", "{4AC3138B-6FD1-4620-A75A-3FCACE995162}"
181+
EndProject
182+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse", "Sample\Warehouse\Warehouse\Warehouse.csproj", "{C45ACE62-41BA-49D9-956A-39B479D7A50A}"
183+
EndProject
184+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse.Api", "Sample\Warehouse\Warehouse.Api\Warehouse.Api.csproj", "{76C04CB6-32C7-47EA-884A-6343BDD39644}"
185+
EndProject
186+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse.Api.Tests", "Sample\Warehouse\Warehouse.Api.Tests\Warehouse.Api.Tests.csproj", "{69B22937-CA8B-478D-97F8-4D33558B5BC9}"
187+
EndProject
180188
Global
181189
GlobalSection(SolutionConfigurationPlatforms) = preSolution
182190
Debug|Any CPU = Debug|Any CPU
@@ -419,6 +427,18 @@ Global
419427
{5C54B28C-7746-4DD0-865E-1AC0D8E8D46B}.Debug|Any CPU.Build.0 = Debug|Any CPU
420428
{5C54B28C-7746-4DD0-865E-1AC0D8E8D46B}.Release|Any CPU.ActiveCfg = Release|Any CPU
421429
{5C54B28C-7746-4DD0-865E-1AC0D8E8D46B}.Release|Any CPU.Build.0 = Release|Any CPU
430+
{C45ACE62-41BA-49D9-956A-39B479D7A50A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
431+
{C45ACE62-41BA-49D9-956A-39B479D7A50A}.Debug|Any CPU.Build.0 = Debug|Any CPU
432+
{C45ACE62-41BA-49D9-956A-39B479D7A50A}.Release|Any CPU.ActiveCfg = Release|Any CPU
433+
{C45ACE62-41BA-49D9-956A-39B479D7A50A}.Release|Any CPU.Build.0 = Release|Any CPU
434+
{76C04CB6-32C7-47EA-884A-6343BDD39644}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
435+
{76C04CB6-32C7-47EA-884A-6343BDD39644}.Debug|Any CPU.Build.0 = Debug|Any CPU
436+
{76C04CB6-32C7-47EA-884A-6343BDD39644}.Release|Any CPU.ActiveCfg = Release|Any CPU
437+
{76C04CB6-32C7-47EA-884A-6343BDD39644}.Release|Any CPU.Build.0 = Release|Any CPU
438+
{69B22937-CA8B-478D-97F8-4D33558B5BC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
439+
{69B22937-CA8B-478D-97F8-4D33558B5BC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
440+
{69B22937-CA8B-478D-97F8-4D33558B5BC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
441+
{69B22937-CA8B-478D-97F8-4D33558B5BC9}.Release|Any CPU.Build.0 = Release|Any CPU
422442
EndGlobalSection
423443
GlobalSection(SolutionProperties) = preSolution
424444
HideSolutionNode = FALSE
@@ -496,6 +516,10 @@ Global
496516
{C9011E7F-42EA-4FEE-A5BB-EFC11BBA0DB0} = {94524EA9-A4BA-4684-99B8-BBE9EE85E791}
497517
{7ACC398F-87BF-4B3E-AD61-DFB5F56D4B25} = {C9011E7F-42EA-4FEE-A5BB-EFC11BBA0DB0}
498518
{03D0848C-7B19-4685-BA1F-59FFAF1DCEA6} = {C9011E7F-42EA-4FEE-A5BB-EFC11BBA0DB0}
519+
{4AC3138B-6FD1-4620-A75A-3FCACE995162} = {A7186B6B-D56D-4AEF-B6B7-FAA827764C34}
520+
{C45ACE62-41BA-49D9-956A-39B479D7A50A} = {4AC3138B-6FD1-4620-A75A-3FCACE995162}
521+
{76C04CB6-32C7-47EA-884A-6343BDD39644} = {4AC3138B-6FD1-4620-A75A-3FCACE995162}
522+
{69B22937-CA8B-478D-97F8-4D33558B5BC9} = {4AC3138B-6FD1-4620-A75A-3FCACE995162}
499523
EndGlobalSection
500524
GlobalSection(ExtensibilityGlobals) = postSolution
501525
SolutionGuid = {A5F55604-2FF3-43B7-B657-4F18E6E95D3B}

Sample/MeetingsManagement/MeetingsManagement.IntegrationTests/Meetings/CreateMeetingTests.cs

-3
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ public CreateMeetingTests(CreateMeetingFixture fixture)
4545
}
4646

4747
[Fact]
48-
[Trait("Category", "Exercise")]
4948
public async Task CreateCommand_ShouldReturn_CreatedStatus_With_MeetingId()
5049
{
5150
var commandResponse = fixture.CommandResponse;
@@ -58,7 +57,6 @@ public async Task CreateCommand_ShouldReturn_CreatedStatus_With_MeetingId()
5857
}
5958

6059
[Fact]
61-
[Trait("Category", "Exercise")]
6260
public void CreateCommand_ShouldPublish_MeetingCreateEvent()
6361
{
6462
// assert MeetingCreated event was produced to external bus
@@ -70,7 +68,6 @@ public void CreateCommand_ShouldPublish_MeetingCreateEvent()
7068
}
7169

7270
[Fact]
73-
[Trait("Category", "Exercise")]
7471
public async Task CreateCommand_ShouldUpdateReadModel()
7572
{
7673
// prepare query

Sample/MeetingsManagement/MeetingsManagement.IntegrationTests/Meetings/ScheduleMeetingTests.cs

-3
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ public ScheduleMeetingTests(ScheduleMeetingFixture fixture)
5353
}
5454

5555
[Fact]
56-
[Trait("Category", "Exercise")]
5756
public async Task CreateMeeting_ShouldReturn_CreatedStatus_With_MeetingId()
5857
{
5958
var commandResponse = fixture.CreateMeetingCommandResponse.EnsureSuccessStatusCode();
@@ -64,7 +63,6 @@ public async Task CreateMeeting_ShouldReturn_CreatedStatus_With_MeetingId()
6463
}
6564

6665
[Fact]
67-
[Trait("Category", "Exercise")]
6866
public async Task ScheduleMeeting_ShouldSucceed()
6967
{
7068
var commandResponse = fixture.ScheduleMeetingCommandResponse.EnsureSuccessStatusCode();
@@ -75,7 +73,6 @@ public async Task ScheduleMeeting_ShouldSucceed()
7573
}
7674

7775
[Fact]
78-
[Trait("Category", "Exercise")]
7976
public async Task ScheduleMeeting_ShouldUpdateReadModel()
8077
{
8178
//send query
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using System;
2+
using System.Net;
3+
using System.Threading.Tasks;
4+
using Core.Testing;
5+
using FluentAssertions;
6+
using Microsoft.AspNetCore.Hosting;
7+
using Warehouse.Products.GettingProductDetails;
8+
using Warehouse.Products.RegisteringProduct;
9+
using Xunit;
10+
11+
namespace Warehouse.Api.Tests.Products.GettingProductDetails
12+
{
13+
public class GetProductDetailsFixture: ApiFixture
14+
{
15+
protected override string ApiUrl => "/api/products";
16+
17+
protected override Func<IWebHostBuilder, IWebHostBuilder> SetupWebHostBuilder =>
18+
whb => WarehouseTestWebHostBuilder.Configure(whb, nameof(GetProductDetailsFixture));
19+
20+
public ProductDetails ExistingProduct = default!;
21+
22+
public Guid ProductId = default!;
23+
24+
public override async Task InitializeAsync()
25+
{
26+
var registerProduct = new RegisterProductRequest("IN11111", "ValidName", "ValidDescription");
27+
var registerResponse = await Post(registerProduct);
28+
29+
registerResponse.EnsureSuccessStatusCode()
30+
.StatusCode.Should().Be(HttpStatusCode.Created);
31+
32+
ProductId = await registerResponse.GetResultFromJson<Guid>();
33+
34+
var (sku, name, description) = registerProduct;
35+
ExistingProduct = new ProductDetails(ProductId, sku!, name!, description);
36+
}
37+
}
38+
39+
public class GetProductDetailsTests: IClassFixture<GetProductDetailsFixture>
40+
{
41+
private readonly GetProductDetailsFixture fixture;
42+
43+
public GetProductDetailsTests(GetProductDetailsFixture fixture)
44+
{
45+
this.fixture = fixture;
46+
}
47+
48+
[Fact]
49+
public async Task ValidRequest_With_NoParams_ShouldReturn_200()
50+
{
51+
// Given
52+
53+
// When
54+
var response = await fixture.Get(fixture.ProductId.ToString());
55+
56+
// Then
57+
response.EnsureSuccessStatusCode()
58+
.StatusCode.Should().Be(HttpStatusCode.OK);
59+
60+
var product = await response.GetResultFromJson<ProductDetails>();
61+
product.Should().NotBeNull();
62+
product.Should().BeEquivalentTo(fixture.ExistingProduct);
63+
}
64+
65+
[Theory]
66+
[InlineData(12)]
67+
[InlineData("not-a-guid")]
68+
public async Task InvalidGuidId_ShouldReturn_400(object invalidId)
69+
{
70+
// Given
71+
72+
// When
73+
var response = await fixture.Get($"{invalidId}");
74+
75+
// Then
76+
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
77+
}
78+
79+
[Fact]
80+
public async Task NotExistingId_ShouldReturn_404()
81+
{
82+
// Given
83+
var notExistingId = Guid.NewGuid();
84+
85+
// When
86+
var response = await fixture.Get($"{notExistingId}");
87+
88+
// Then
89+
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
90+
}
91+
}
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Threading.Tasks;
6+
using Core.Testing;
7+
using FluentAssertions;
8+
using Microsoft.AspNetCore.Hosting;
9+
using Warehouse.Products.GettingProducts;
10+
using Warehouse.Products.RegisteringProduct;
11+
using Xunit;
12+
13+
namespace Warehouse.Api.Tests.Products.GettingProducts
14+
{
15+
public class GetProductsFixture: ApiFixture
16+
{
17+
protected override string ApiUrl => "/api/products";
18+
19+
protected override Func<IWebHostBuilder, IWebHostBuilder> SetupWebHostBuilder =>
20+
whb => WarehouseTestWebHostBuilder.Configure(whb, nameof(GetProductsFixture));
21+
22+
public IList<ProductListItem> RegisteredProducts = new List<ProductListItem>();
23+
24+
public override async Task InitializeAsync()
25+
{
26+
var productsToRegister = new[]
27+
{
28+
new RegisterProductRequest("ZX1234", "ValidName", "ValidDescription"),
29+
new RegisterProductRequest("AD5678", "OtherValidName", "OtherValidDescription"),
30+
new RegisterProductRequest("BH90210", "AnotherValid", "AnotherValidDescription")
31+
};
32+
33+
foreach (var registerProduct in productsToRegister)
34+
{
35+
var registerResponse = await Post(registerProduct);
36+
registerResponse.EnsureSuccessStatusCode()
37+
.StatusCode.Should().Be(HttpStatusCode.Created);
38+
39+
var createdId = await registerResponse.GetResultFromJson<Guid>();
40+
41+
var (sku, name, _) = registerProduct;
42+
RegisteredProducts.Add(new ProductListItem(createdId, sku!, name!));
43+
}
44+
}
45+
}
46+
47+
public class GetProductsTests: IClassFixture<GetProductsFixture>
48+
{
49+
private readonly GetProductsFixture fixture;
50+
51+
public GetProductsTests(GetProductsFixture fixture)
52+
{
53+
this.fixture = fixture;
54+
}
55+
56+
[Fact]
57+
public async Task ValidRequest_With_NoParams_ShouldReturn_200()
58+
{
59+
// Given
60+
61+
// When
62+
var response = await fixture.Get();
63+
64+
// Then
65+
response.EnsureSuccessStatusCode()
66+
.StatusCode.Should().Be(HttpStatusCode.OK);
67+
68+
var products = await response.GetResultFromJson<IReadOnlyList<ProductListItem>>();
69+
products.Should().NotBeEmpty();
70+
products.Should().BeEquivalentTo(fixture.RegisteredProducts);
71+
}
72+
73+
[Fact]
74+
public async Task ValidRequest_With_Filter_ShouldReturn_SubsetOfRecords()
75+
{
76+
// Given
77+
var filteredRecord = fixture.RegisteredProducts.First();
78+
var filter = fixture.RegisteredProducts.First().Sku.Substring(1);
79+
80+
// When
81+
var response = await fixture.Get($"?filter={filter}");
82+
83+
// Then
84+
response.EnsureSuccessStatusCode()
85+
.StatusCode.Should().Be(HttpStatusCode.OK);
86+
87+
var products = await response.GetResultFromJson<IReadOnlyList<ProductListItem>>();
88+
products.Should().NotBeEmpty();
89+
products.Should().BeEquivalentTo(new List<ProductListItem>{filteredRecord});
90+
}
91+
92+
93+
94+
[Fact]
95+
public async Task ValidRequest_With_Paging_ShouldReturn_PageOfRecords()
96+
{
97+
// Given
98+
const int page = 2;
99+
const int pageSize = 1;
100+
var filteredRecords = fixture.RegisteredProducts
101+
.Skip(page - 1)
102+
.Take(pageSize)
103+
.ToList();
104+
105+
// When
106+
var response = await fixture.Get($"?page={page}&pageSize={pageSize}");
107+
108+
// Then
109+
response.EnsureSuccessStatusCode()
110+
.StatusCode.Should().Be(HttpStatusCode.OK);
111+
112+
var products = await response.GetResultFromJson<IReadOnlyList<ProductListItem>>();
113+
products.Should().NotBeEmpty();
114+
products.Should().BeEquivalentTo(filteredRecords);
115+
}
116+
117+
[Fact]
118+
public async Task NegativePage_ShouldReturn_400()
119+
{
120+
// Given
121+
var pageSize = -20;
122+
123+
// When
124+
var response = await fixture.Get($"?page={pageSize}");
125+
126+
// Then
127+
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
128+
}
129+
130+
[Theory]
131+
[InlineData(0)]
132+
[InlineData(-20)]
133+
public async Task NegativeOrZeroPageSize_ShouldReturn_400(int pageSize)
134+
{
135+
// Given
136+
137+
// When
138+
var response = await fixture.Get($"?page={pageSize}");
139+
140+
// Then
141+
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)