From f543c2b51a88dd00b4dd34f1e7c264793f54816d Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 21 Nov 2024 19:01:01 +1100 Subject: [PATCH 01/26] Add EventStore Hosting and Client integrations --- CommunityToolkit.Aspire.sln | 77 +++++-- Directory.Packages.props | 3 + .../Account.cs | 105 +++++++++ .../AccountEvents.cs | 7 + ...spire.Hosting.EventStore.ApiService.csproj | 18 ++ .../EventStoreExtensions.cs | 76 +++++++ .../Program.cs | 81 +++++++ .../Properties/launchSettings.json | 41 ++++ .../appsettings.json | 9 + ...t.Aspire.Hosting.EventStore.AppHost.csproj | 21 ++ .../Program.cs | 11 + .../Properties/launchSettings.json | 29 +++ .../appsettings.json | 9 + ....Hosting.EventStore.ServiceDefaults.csproj | 21 ++ .../Extensions.cs | 117 ++++++++++ .../AspireEventStoreExtensions.cs | 106 +++++++++ .../CommunityToolkit.Aspire.EventStore.csproj | 19 ++ .../EventStoreSettings.cs | 31 +++ .../PublicAPI.Shipped.txt | 12 + .../PublicAPI.Unshipped.txt | 2 + .../README.md | 114 ++++++++++ ...tyToolkit.Aspire.Hosting.EventStore.csproj | 20 ++ .../EventStoreBuilderExtensions.cs | 210 ++++++++++++++++++ .../EventStoreContainerImageTags.cs | 11 + .../EventStoreResource.cs | 28 +++ .../PublicAPI.Shipped.txt | 11 + .../PublicAPI.Unshipped.txt | 2 + .../README.md | 37 +++ .../AddEventStoreTests.cs | 65 ++++++ .../AppHostTests.cs | 109 +++++++++ ...kit.Aspire.Hosting.EventStore.Tests.csproj | 19 ++ .../EventStorePublicApiTests.cs | 83 +++++++ 32 files changed, 1488 insertions(+), 16 deletions(-) create mode 100644 examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/Account.cs create mode 100644 examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/AccountEvents.cs create mode 100644 examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/CommunityToolkit.Aspire.Hosting.EventStore.ApiService.csproj create mode 100644 examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/EventStoreExtensions.cs create mode 100644 examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/Program.cs create mode 100644 examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/Properties/launchSettings.json create mode 100644 examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/appsettings.json create mode 100644 examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/CommunityToolkit.Aspire.Hosting.EventStore.AppHost.csproj create mode 100644 examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/Program.cs create mode 100644 examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/Properties/launchSettings.json create mode 100644 examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/appsettings.json create mode 100644 examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults.csproj create mode 100644 examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults/Extensions.cs create mode 100644 src/CommunityToolkit.Aspire.EventStore/AspireEventStoreExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.EventStore/CommunityToolkit.Aspire.EventStore.csproj create mode 100644 src/CommunityToolkit.Aspire.EventStore/EventStoreSettings.cs create mode 100644 src/CommunityToolkit.Aspire.EventStore/PublicAPI.Shipped.txt create mode 100644 src/CommunityToolkit.Aspire.EventStore/PublicAPI.Unshipped.txt create mode 100644 src/CommunityToolkit.Aspire.EventStore/README.md create mode 100644 src/CommunityToolkit.Aspire.Hosting.EventStore/CommunityToolkit.Aspire.Hosting.EventStore.csproj create mode 100644 src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreContainerImageTags.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreResource.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.EventStore/PublicAPI.Shipped.txt create mode 100644 src/CommunityToolkit.Aspire.Hosting.EventStore/PublicAPI.Unshipped.txt create mode 100644 src/CommunityToolkit.Aspire.Hosting.EventStore/README.md create mode 100644 tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AppHostTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests.csproj create mode 100644 tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStorePublicApiTests.cs diff --git a/CommunityToolkit.Aspire.sln b/CommunityToolkit.Aspire.sln index acee2e08..5bf544f8 100644 --- a/CommunityToolkit.Aspire.sln +++ b/CommunityToolkit.Aspire.sln @@ -145,6 +145,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hos EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Bun.Tests", "tests\CommunityToolkit.Aspire.Hosting.Bun.Tests\CommunityToolkit.Aspire.Hosting.Bun.Tests.csproj", "{DA5DD2CB-51D9-429F-91F5-BF3D1A13A21A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eventstore", "eventstore", "{114DDF07-489A-419B-BE76-E5A289F12791}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.EventStore", "src\CommunityToolkit.Aspire.Hosting.EventStore\CommunityToolkit.Aspire.Hosting.EventStore.csproj", "{B209275E-1CFF-4AF0-A65A-2895DD679775}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.EventStore", "src\CommunityToolkit.Aspire.EventStore\CommunityToolkit.Aspire.EventStore.csproj", "{AD230A69-F6AE-4A9B-B500-90516BA2E1C6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.EventStore.Tests", "tests\CommunityToolkit.Aspire.Hosting.EventStore.Tests\CommunityToolkit.Aspire.Hosting.EventStore.Tests.csproj", "{FA34A40C-62C9-4A73-A39D-53A01243657C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.EventStore.AppHost", "examples\eventstore\CommunityToolkit.Aspire.Hosting.EventStore.AppHost\CommunityToolkit.Aspire.Hosting.EventStore.AppHost.csproj", "{ED3E5B89-091C-4A0E-9A2B-946CA1A11557}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults", "examples\eventstore\CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults\CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults.csproj", "{0A4E5B43-155A-4FDA-A50F-0B86CF1705E7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.EventStore.ApiService", "examples\eventstore\CommunityToolkit.Aspire.Hosting.EventStore.ApiService\CommunityToolkit.Aspire.Hosting.EventStore.ApiService.csproj", "{019D6506-9D68-41AD-A7A1-A27B2FFE1253}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -327,18 +341,6 @@ Global {C7D057AF-E2A5-4E26-846E-A328A0F14A3C}.Debug|Any CPU.Build.0 = Debug|Any CPU {C7D057AF-E2A5-4E26-846E-A328A0F14A3C}.Release|Any CPU.ActiveCfg = Release|Any CPU {C7D057AF-E2A5-4E26-846E-A328A0F14A3C}.Release|Any CPU.Build.0 = Release|Any CPU - {4DCF987E-9071-4899-8B5F-5FDAF2BC77D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4DCF987E-9071-4899-8B5F-5FDAF2BC77D3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4DCF987E-9071-4899-8B5F-5FDAF2BC77D3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4DCF987E-9071-4899-8B5F-5FDAF2BC77D3}.Release|Any CPU.Build.0 = Release|Any CPU - {C686CEA0-8B89-470B-84A2-0264040DCDC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C686CEA0-8B89-470B-84A2-0264040DCDC8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C686CEA0-8B89-470B-84A2-0264040DCDC8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C686CEA0-8B89-470B-84A2-0264040DCDC8}.Release|Any CPU.Build.0 = Release|Any CPU - {5B825CF9-E8B8-4960-9330-648ED0323FE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5B825CF9-E8B8-4960-9330-648ED0323FE0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5B825CF9-E8B8-4960-9330-648ED0323FE0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5B825CF9-E8B8-4960-9330-648ED0323FE0}.Release|Any CPU.Build.0 = Release|Any CPU {6BC98146-279F-4DE5-9B6E-0F0C07507421}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6BC98146-279F-4DE5-9B6E-0F0C07507421}.Debug|Any CPU.Build.0 = Debug|Any CPU {6BC98146-279F-4DE5-9B6E-0F0C07507421}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -359,6 +361,18 @@ Global {2CC61B84-CF97-4CE7-A08F-2EECF4AEAD92}.Debug|Any CPU.Build.0 = Debug|Any CPU {2CC61B84-CF97-4CE7-A08F-2EECF4AEAD92}.Release|Any CPU.ActiveCfg = Release|Any CPU {2CC61B84-CF97-4CE7-A08F-2EECF4AEAD92}.Release|Any CPU.Build.0 = Release|Any CPU + {4DCF987E-9071-4899-8B5F-5FDAF2BC77D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4DCF987E-9071-4899-8B5F-5FDAF2BC77D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4DCF987E-9071-4899-8B5F-5FDAF2BC77D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4DCF987E-9071-4899-8B5F-5FDAF2BC77D3}.Release|Any CPU.Build.0 = Release|Any CPU + {C686CEA0-8B89-470B-84A2-0264040DCDC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C686CEA0-8B89-470B-84A2-0264040DCDC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C686CEA0-8B89-470B-84A2-0264040DCDC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C686CEA0-8B89-470B-84A2-0264040DCDC8}.Release|Any CPU.Build.0 = Release|Any CPU + {5B825CF9-E8B8-4960-9330-648ED0323FE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B825CF9-E8B8-4960-9330-648ED0323FE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B825CF9-E8B8-4960-9330-648ED0323FE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B825CF9-E8B8-4960-9330-648ED0323FE0}.Release|Any CPU.Build.0 = Release|Any CPU {6095E8B8-7F99-4A12-B7E2-376F7EDD7435}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6095E8B8-7F99-4A12-B7E2-376F7EDD7435}.Debug|Any CPU.Build.0 = Debug|Any CPU {6095E8B8-7F99-4A12-B7E2-376F7EDD7435}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -371,6 +385,30 @@ Global {DA5DD2CB-51D9-429F-91F5-BF3D1A13A21A}.Debug|Any CPU.Build.0 = Debug|Any CPU {DA5DD2CB-51D9-429F-91F5-BF3D1A13A21A}.Release|Any CPU.ActiveCfg = Release|Any CPU {DA5DD2CB-51D9-429F-91F5-BF3D1A13A21A}.Release|Any CPU.Build.0 = Release|Any CPU + {B209275E-1CFF-4AF0-A65A-2895DD679775}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B209275E-1CFF-4AF0-A65A-2895DD679775}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B209275E-1CFF-4AF0-A65A-2895DD679775}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B209275E-1CFF-4AF0-A65A-2895DD679775}.Release|Any CPU.Build.0 = Release|Any CPU + {AD230A69-F6AE-4A9B-B500-90516BA2E1C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD230A69-F6AE-4A9B-B500-90516BA2E1C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD230A69-F6AE-4A9B-B500-90516BA2E1C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD230A69-F6AE-4A9B-B500-90516BA2E1C6}.Release|Any CPU.Build.0 = Release|Any CPU + {FA34A40C-62C9-4A73-A39D-53A01243657C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA34A40C-62C9-4A73-A39D-53A01243657C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA34A40C-62C9-4A73-A39D-53A01243657C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA34A40C-62C9-4A73-A39D-53A01243657C}.Release|Any CPU.Build.0 = Release|Any CPU + {ED3E5B89-091C-4A0E-9A2B-946CA1A11557}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED3E5B89-091C-4A0E-9A2B-946CA1A11557}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED3E5B89-091C-4A0E-9A2B-946CA1A11557}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED3E5B89-091C-4A0E-9A2B-946CA1A11557}.Release|Any CPU.Build.0 = Release|Any CPU + {0A4E5B43-155A-4FDA-A50F-0B86CF1705E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A4E5B43-155A-4FDA-A50F-0B86CF1705E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A4E5B43-155A-4FDA-A50F-0B86CF1705E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A4E5B43-155A-4FDA-A50F-0B86CF1705E7}.Release|Any CPU.Build.0 = Release|Any CPU + {019D6506-9D68-41AD-A7A1-A27B2FFE1253}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {019D6506-9D68-41AD-A7A1-A27B2FFE1253}.Debug|Any CPU.Build.0 = Debug|Any CPU + {019D6506-9D68-41AD-A7A1-A27B2FFE1253}.Release|Any CPU.ActiveCfg = Release|Any CPU + {019D6506-9D68-41AD-A7A1-A27B2FFE1253}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -429,10 +467,6 @@ Global {4AE83D68-EA10-473D-BD26-19C5928A8620} = {8519CC01-1370-47C8-AD94-B0F326B1563F} {79EF8E85-1DFC-42B5-BDE3-72639F25848C} = {4AE83D68-EA10-473D-BD26-19C5928A8620} {C7D057AF-E2A5-4E26-846E-A328A0F14A3C} = {899F0713-7FC6-4750-BAFC-AC650B35B453} - {4DCF987E-9071-4899-8B5F-5FDAF2BC77D3} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} - {DDDAABA3-D8F0-47C6-98E0-AB57F28404CF} = {8519CC01-1370-47C8-AD94-B0F326B1563F} - {C686CEA0-8B89-470B-84A2-0264040DCDC8} = {DDDAABA3-D8F0-47C6-98E0-AB57F28404CF} - {5B825CF9-E8B8-4960-9330-648ED0323FE0} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {6BC98146-279F-4DE5-9B6E-0F0C07507421} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} {662514C8-EAED-4EAB-91CE-893D4DE2469A} = {8519CC01-1370-47C8-AD94-B0F326B1563F} {1E753568-E34B-4E93-93F8-43764171725D} = {662514C8-EAED-4EAB-91CE-893D4DE2469A} @@ -440,10 +474,21 @@ Global {373472DA-BAEB-44B6-915D-1EF3DA845797} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {79CBF217-CED1-4BB2-9A72-37D2429F83B8} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {2CC61B84-CF97-4CE7-A08F-2EECF4AEAD92} = {79CBF217-CED1-4BB2-9A72-37D2429F83B8} + {4DCF987E-9071-4899-8B5F-5FDAF2BC77D3} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} + {DDDAABA3-D8F0-47C6-98E0-AB57F28404CF} = {8519CC01-1370-47C8-AD94-B0F326B1563F} + {C686CEA0-8B89-470B-84A2-0264040DCDC8} = {DDDAABA3-D8F0-47C6-98E0-AB57F28404CF} + {5B825CF9-E8B8-4960-9330-648ED0323FE0} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {6095E8B8-7F99-4A12-B7E2-376F7EDD7435} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} {A7614F2B-E810-412E-91E7-8B6272DD5DBB} = {8519CC01-1370-47C8-AD94-B0F326B1563F} {36FC2579-582A-4DAF-9B20-AB33331624C6} = {A7614F2B-E810-412E-91E7-8B6272DD5DBB} {DA5DD2CB-51D9-429F-91F5-BF3D1A13A21A} = {899F0713-7FC6-4750-BAFC-AC650B35B453} + {114DDF07-489A-419B-BE76-E5A289F12791} = {8519CC01-1370-47C8-AD94-B0F326B1563F} + {B209275E-1CFF-4AF0-A65A-2895DD679775} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} + {AD230A69-F6AE-4A9B-B500-90516BA2E1C6} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} + {FA34A40C-62C9-4A73-A39D-53A01243657C} = {899F0713-7FC6-4750-BAFC-AC650B35B453} + {ED3E5B89-091C-4A0E-9A2B-946CA1A11557} = {114DDF07-489A-419B-BE76-E5A289F12791} + {0A4E5B43-155A-4FDA-A50F-0B86CF1705E7} = {114DDF07-489A-419B-BE76-E5A289F12791} + {019D6506-9D68-41AD-A7A1-A27B2FFE1253} = {114DDF07-489A-419B-BE76-E5A289F12791} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {08B1D4B8-D2C5-4A64-BB8B-E1A2B29525F0} diff --git a/Directory.Packages.props b/Directory.Packages.props index 66118f90..2362e4cb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,6 +10,7 @@ + @@ -46,6 +47,8 @@ + + diff --git a/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/Account.cs b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/Account.cs new file mode 100644 index 00000000..c42c385d --- /dev/null +++ b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/Account.cs @@ -0,0 +1,105 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Hosting.EventStore.ApiService; + +public class Account +{ + public Guid Id { get; private set; } + public string? Name { get; private set; } + public decimal Balance { get; private set; } + + [JsonIgnore] + public int Version { get; private set; } = -1; + + [NonSerialized] + private readonly Queue uncommittedEvents = new(); + + public static Account Create(Guid id, string name) + => new(id, name); + + public void Deposit(decimal amount) + { + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(amount, 0, nameof(amount)); + + var @event = new AccountFundsDeposited(Id, amount); + + uncommittedEvents.Enqueue(@event); + Apply(@event); + } + + public void Withdraw(decimal amount) + { + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(amount, 0, nameof(amount)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(amount, Balance, nameof(amount)); + + var @event = new AccountFundsWithdrew(Id, amount); + + uncommittedEvents.Enqueue(@event); + Apply(@event); + } + + public void When(object @event) + { + switch (@event) + { + case AccountCreated accountCreated: + Apply(accountCreated); + break; + case AccountFundsDeposited accountFundsDeposited: + Apply(accountFundsDeposited); + break; + case AccountFundsWithdrew accountFundsWithdrew: + Apply(accountFundsWithdrew); + break; + } + } + + public object[] DequeueUncommittedEvents() + { + var dequeuedEvents = uncommittedEvents.ToArray(); + + uncommittedEvents.Clear(); + + return dequeuedEvents; + } + + private Account() + { + } + + private Account(Guid id, string name) + { + if (id == Guid.Empty) + { + throw new ArgumentException("Id cannot be empty.", nameof(id)); + } + ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name)); + + var @event = new AccountCreated(id, name); + + uncommittedEvents.Enqueue(@event); + Apply(@event); + } + + private void Apply(AccountCreated @event) + { + Version++; + + Id = @event.Id; + Name = @event.Name; + } + + private void Apply(AccountFundsDeposited @event) + { + Version++; + + Balance += @event.Amount; + } + + private void Apply(AccountFundsWithdrew @event) + { + Version++; + + Balance -= @event.Amount; + } +} diff --git a/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/AccountEvents.cs b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/AccountEvents.cs new file mode 100644 index 00000000..14aad68e --- /dev/null +++ b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/AccountEvents.cs @@ -0,0 +1,7 @@ +namespace CommunityToolkit.Aspire.Hosting.EventStore.ApiService; + +public record AccountCreated(Guid Id, string Name); + +public record AccountFundsDeposited(Guid Id, decimal Amount); + +public record AccountFundsWithdrew(Guid Id, decimal Amount); diff --git a/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/CommunityToolkit.Aspire.Hosting.EventStore.ApiService.csproj b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/CommunityToolkit.Aspire.Hosting.EventStore.ApiService.csproj new file mode 100644 index 00000000..75fb8674 --- /dev/null +++ b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/CommunityToolkit.Aspire.Hosting.EventStore.ApiService.csproj @@ -0,0 +1,18 @@ + + + + enable + enable + + + + + + + + + + + + + diff --git a/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/EventStoreExtensions.cs b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/EventStoreExtensions.cs new file mode 100644 index 00000000..eee3158f --- /dev/null +++ b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/EventStoreExtensions.cs @@ -0,0 +1,76 @@ +using EventStore.Client; +using System.Text.Json; +using System.Text; + +namespace CommunityToolkit.Aspire.Hosting.EventStore.ApiService; + +public static class EventStoreExtensions +{ + public static async Task GetAccount(this EventStoreClient eventStore, Guid id, CancellationToken cancellationToken) + { + var readResult = eventStore.ReadStreamAsync( + Direction.Forwards, + $"account-{id:N}", + StreamPosition.Start, + cancellationToken: cancellationToken + ); + + var readState = await readResult.ReadState; + if (readState == ReadState.StreamNotFound) + { + return null; + } + + var account = (Account)Activator.CreateInstance(typeof(Account), true)!; + + await foreach (var resolvedEvent in readResult) + { + var @event = resolvedEvent.Deserialize(); + + account.When(@event!); + } + + return account; + } + + public static async Task AppendAcountEvents(this EventStoreClient eventStore, Account account, CancellationToken cancellationToken) + { + var events = account.DequeueUncommittedEvents(); + + var eventsToAppend = events + .Select(@event => @event.Serialize()).ToArray(); + + var expectedVersion = account.Version - events.Length; + await eventStore.AppendToStreamAsync( + $"account-{account.Id:N}", + expectedVersion == 0 ? StreamRevision.None : StreamRevision.FromInt64(expectedVersion), + eventsToAppend, + cancellationToken: cancellationToken + ); + } + + private static object? Deserialize(this ResolvedEvent resolvedEvent) + { + var eventClrTypeName = JsonDocument.Parse(resolvedEvent.Event.Metadata) + .RootElement + .GetProperty("EventClrTypeName") + .GetString(); + + return JsonSerializer.Deserialize( + Encoding.UTF8.GetString(resolvedEvent.Event.Data.Span), + Type.GetType(eventClrTypeName!)!); + } + + private static EventData Serialize(this object @event) + { + return new EventData( + Uuid.NewUuid(), + @event.GetType().Name, + data: Encoding.UTF8.GetBytes(JsonSerializer.Serialize(@event)), + metadata: Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new Dictionary + { + { "EventClrTypeName", @event.GetType().AssemblyQualifiedName! } + })) + ); + } +} diff --git a/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/Program.cs b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/Program.cs new file mode 100644 index 00000000..c87a791a --- /dev/null +++ b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/Program.cs @@ -0,0 +1,81 @@ +using CommunityToolkit.Aspire.Hosting.EventStore.ApiService; +using EventStore.Client; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.AddEventStoreClient("eventstore"); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +app.UseHttpsRedirection(); + +app.MapDefaultEndpoints(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.MapPost("/account/create", async (EventStoreClient eventStore, CancellationToken cancellationToken) => +{ + var account = Account.Create(Guid.NewGuid(), "John Doe"); + + account.Deposit(100); + + await eventStore.AppendAcountEvents(account, cancellationToken); + + return Results.Created($"/account/{account.Id}", account); +}); + +app.MapGet("/account/{id:guid}", async (Guid id, EventStoreClient eventStore, CancellationToken cancellationToken) => +{ + var account = await eventStore.GetAccount(id, cancellationToken); + if (account is null) + { + return Results.NotFound(); + } + + return TypedResults.Ok(account); +}); + +app.MapPost("/account/{id:guid}/deposit", async (Guid id, DepositRequest request, EventStoreClient eventStore, CancellationToken cancellationToken) => +{ + var account = await eventStore.GetAccount(id, cancellationToken); + if (account is null) + { + return Results.NotFound(); + } + + account.Deposit(request.Amount); + + await eventStore.AppendAcountEvents(account, cancellationToken); + + return Results.Ok(); +}); + +app.MapPost("/account/{id:guid}/withdraw", async (Guid id, WithdrawRequest request, EventStoreClient eventStore, CancellationToken cancellationToken) => +{ + var account = await eventStore.GetAccount(id, cancellationToken); + if (account is null) + { + return Results.NotFound(); + } + + account.Withdraw(request.Amount); + + await eventStore.AppendAcountEvents(account, cancellationToken); + + return Results.Ok(); +}); + +app.Run(); + +public record DepositRequest(decimal Amount); +public record WithdrawRequest(decimal Amount); diff --git a/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/Properties/launchSettings.json b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/Properties/launchSettings.json new file mode 100644 index 00000000..08fc4374 --- /dev/null +++ b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:38959", + "sslPort": 44303 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5279", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7015;http://localhost:5279", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/appsettings.json b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ApiService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/CommunityToolkit.Aspire.Hosting.EventStore.AppHost.csproj b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/CommunityToolkit.Aspire.Hosting.EventStore.AppHost.csproj new file mode 100644 index 00000000..b620b267 --- /dev/null +++ b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/CommunityToolkit.Aspire.Hosting.EventStore.AppHost.csproj @@ -0,0 +1,21 @@ + + + + + Exe + enable + enable + true + 9ea31b5e-317f-4692-8a61-e60ac7ec0d0a + + + + + + + + + + + + diff --git a/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/Program.cs b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/Program.cs new file mode 100644 index 00000000..3bbc58f7 --- /dev/null +++ b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/Program.cs @@ -0,0 +1,11 @@ +using Projects; + +var builder = DistributedApplication.CreateBuilder(args); + +var eventstore = builder.AddEventStore("eventstore", 22113); + +builder.AddProject("apiservice") + .WithReference(eventstore) + .WaitFor(eventstore); + +builder.Build().Run(); diff --git a/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/Properties/launchSettings.json b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..f996ed79 --- /dev/null +++ b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17038;http://localhost:15090", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21125", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22133" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15090", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19068", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20178" + } + } + } +} diff --git a/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/appsettings.json b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults.csproj b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults.csproj new file mode 100644 index 00000000..caa6344d --- /dev/null +++ b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults.csproj @@ -0,0 +1,21 @@ + + + + enable + enable + true + + + + + + + + + + + + + + + diff --git a/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults/Extensions.cs b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..1081a52f --- /dev/null +++ b/examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults/Extensions.cs @@ -0,0 +1,117 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/src/CommunityToolkit.Aspire.EventStore/AspireEventStoreExtensions.cs b/src/CommunityToolkit.Aspire.EventStore/AspireEventStoreExtensions.cs new file mode 100644 index 00000000..f227fcb0 --- /dev/null +++ b/src/CommunityToolkit.Aspire.EventStore/AspireEventStoreExtensions.cs @@ -0,0 +1,106 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire; +using CommunityToolkit.Aspire.EventStore; +using EventStore.Client; +using EventStore.Client.Extensions.OpenTelemetry; +using HealthChecks.EventStore.gRPC; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.Hosting; + +/// +/// Provides extension methods for registering EventStore-related services in an . +/// +public static class AspireEventStoreExtensions +{ + private const string DefaultConfigSectionName = "Aspire:EventStore:Client"; + + /// + /// Registers as a singleton in the services provided by the . + /// + /// The to read config from and add services to. + /// The connection name to use to find a connection string. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + public static void AddEventStoreClient( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNullOrEmpty(connectionName); + AddEventStoreClient(builder, configureSettings, connectionName, serviceKey: null); + } + + /// + /// Registers as a keyed singleton for the given in the services provided by the . + /// + /// The to read config from and add services to. + /// The connection name to use to find a connection string. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + public static void AddKeyedEventStoreClient( + this IHostApplicationBuilder builder, + string name, + Action? configureSettings = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNullOrEmpty(name); + AddEventStoreClient(builder, configureSettings, connectionName: name, serviceKey: name); + } + + private static void AddEventStoreClient( + this IHostApplicationBuilder builder, + Action? configureSettings, + string connectionName, + string? serviceKey) + { + ArgumentNullException.ThrowIfNull(builder); + + var configSection = builder.Configuration.GetSection(DefaultConfigSectionName); + var namedConfigSection = configSection.GetSection(connectionName); + + var settings = new EventStoreSettings(); + configSection.Bind(settings); + namedConfigSection.Bind(settings); + + if (builder.Configuration.GetConnectionString(connectionName) is string connectionString) + { + settings.ConnectionString = connectionString; + } + + configureSettings?.Invoke(settings); + + var eventStoreClientSettings = EventStoreClientSettings.Create(settings.ConnectionString!); + var eventStoreClient = new EventStoreClient(eventStoreClientSettings); + + if (serviceKey is null) + { + builder.Services.AddSingleton(eventStoreClient); + } + else + { + builder.Services.AddKeyedSingleton(serviceKey, (sp, key) => eventStoreClient); + } + + if (!settings.DisableTracing) + { + builder.Services.AddOpenTelemetry() + .WithTracing(traceBuilder => traceBuilder.AddEventStoreClientInstrumentation()); + } + + if (!settings.DisableHealthChecks) + { + var healthCheckName = serviceKey is null ? "EventStore.Client" : $"EventStore.Client_{connectionName}"; + + builder.TryAddHealthCheck(new HealthCheckRegistration( + healthCheckName, + sp => new EventStoreHealthCheck(settings.ConnectionString!), + failureStatus: default, + tags: default, + timeout: default)); + } + } +} diff --git a/src/CommunityToolkit.Aspire.EventStore/CommunityToolkit.Aspire.EventStore.csproj b/src/CommunityToolkit.Aspire.EventStore/CommunityToolkit.Aspire.EventStore.csproj new file mode 100644 index 00000000..31e7ca06 --- /dev/null +++ b/src/CommunityToolkit.Aspire.EventStore/CommunityToolkit.Aspire.EventStore.csproj @@ -0,0 +1,19 @@ + + + + EventStore client + An EventStore client that integrates with Aspire, including health checks, logging, and telemetry. + + + + + + + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.EventStore/EventStoreSettings.cs b/src/CommunityToolkit.Aspire.EventStore/EventStoreSettings.cs new file mode 100644 index 00000000..0bc199b6 --- /dev/null +++ b/src/CommunityToolkit.Aspire.EventStore/EventStoreSettings.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace CommunityToolkit.Aspire.EventStore; + +/// +/// Provides the client configuration settings for connecting to an EventStore server using EventStoreClient. +/// +public sealed class EventStoreSettings +{ + /// + /// Gets or sets the connection string. + /// + public string? ConnectionString { get; set; } + + /// + /// Gets or sets a boolean value that indicates whether the EventStore health check is disabled or not. + /// + /// + /// The default value is . + /// + public bool DisableHealthChecks { get; set; } + + /// + /// Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is disabled or not. + /// + /// + /// The default value is . + /// + public bool DisableTracing { get; set; } +} diff --git a/src/CommunityToolkit.Aspire.EventStore/PublicAPI.Shipped.txt b/src/CommunityToolkit.Aspire.EventStore/PublicAPI.Shipped.txt new file mode 100644 index 00000000..9aabdbb6 --- /dev/null +++ b/src/CommunityToolkit.Aspire.EventStore/PublicAPI.Shipped.txt @@ -0,0 +1,12 @@ +#nullable enable +CommunityToolkit.Aspire.EventStore.EventStoreSettings +CommunityToolkit.Aspire.EventStore.EventStoreSettings.ConnectionString.get -> string? +CommunityToolkit.Aspire.EventStore.EventStoreSettings.ConnectionString.set -> void +CommunityToolkit.Aspire.EventStore.EventStoreSettings.DisableHealthChecks.get -> bool +CommunityToolkit.Aspire.EventStore.EventStoreSettings.DisableHealthChecks.set -> void +CommunityToolkit.Aspire.EventStore.EventStoreSettings.EventStoreSettings() -> void +CommunityToolkit.Aspire.EventStore.EventStoreSettings.DisableTracing.get -> bool +CommunityToolkit.Aspire.EventStore.EventStoreSettings.DisableTracing.set -> void +Microsoft.Extensions.Hosting.AspireEventStoreExtensions +static Microsoft.Extensions.Hosting.AspireEventStoreExtensions.AddKeyedEventStoreClient(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, string! name, System.Action? configureSettings = null) -> void +static Microsoft.Extensions.Hosting.AspireEventStoreExtensions.AddEventStoreClient(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, string! connectionName, System.Action? configureSettings = null) -> void diff --git a/src/CommunityToolkit.Aspire.EventStore/PublicAPI.Unshipped.txt b/src/CommunityToolkit.Aspire.EventStore/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..074c6ad1 --- /dev/null +++ b/src/CommunityToolkit.Aspire.EventStore/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +#nullable enable + diff --git a/src/CommunityToolkit.Aspire.EventStore/README.md b/src/CommunityToolkit.Aspire.EventStore/README.md new file mode 100644 index 00000000..edad8df0 --- /dev/null +++ b/src/CommunityToolkit.Aspire.EventStore/README.md @@ -0,0 +1,114 @@ +# CommunityToolkit.Aspire.EventStore + +Registers an [EventStoreClient](https://github.com/EventStore/EventStore-Client-Dotnet) in the DI container for connecting to an EventStore. + +## Getting started + +### Prerequisites + +- EventStore cluster. + +### Install the package + +Install the .NET Aspire EventStore Client library with [NuGet](https://www.nuget.org): + +```dotnetcli +dotnet add package CommunityToolkit.Aspire.EventStore +``` + +## Usage example + +In the _Program.cs_ file of your project, call the `AddEventStoreClient` extension method to register an `EventStoreClient` for use via the dependency injection container. The method takes a connection name parameter. + +```csharp +builder.AddEventStoreClient("eventstore"); +``` + +## Configuration + +The .NET Aspire EventStore Client integration provides multiple options to configure the server connection based on the requirements and conventions of your project. + +### Use a connection string + +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddEventStoreClient()`: + +```csharp +builder.AddEventStoreClient("eventstore"); +``` + +And then the connection string will be retrieved from the `ConnectionStrings` configuration section: + +```json +{ + "ConnectionStrings": { + "eventstore": "esdb://localhost:22113?tls=false" + } +} +``` + +### Use configuration providers + +The .NET Aspire EventStore Client integration supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `EventStoreSettings` from configuration by using the `Aspire:EventStore:Client` key. Example `appsettings.json` that configures some of the options: + +```json +{ + "Aspire": { + "EventStore": { + "Client": { + "ConnectionString": "esdb://localhost:22113?tls=false", + "DisableHealthChecks": true + } + } + } +} +``` + +### Use inline delegates + +Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to set the API key from code: + +```csharp +builder.AddEventStoreClient("eventstore", settings => settings.DisableHealthChecks = true); +``` + +## AppHost extensions + +In your AppHost project, install the `CommunityToolkit.Aspire.Hosting.EventStore` library with [NuGet](https://www.nuget.org): + +```dotnetcli +dotnet add package CommunityToolkit.Aspire.Hosting.EventStore +``` + +Then, in the _Program.cs_ file of `AppHost`, register EventStore and consume the connection using the following methods: + +```csharp +var eventstore = builder.AddEventStore("eventstore"); + +var myService = builder.AddProject() + .WithReference(eventstore); +``` + +The `WithReference` method configures a connection in the `MyService` project named `eventstore`. In the _Program.cs_ file of `MyService`, the EventStore connection can be consumed using: + +```csharp +builder.AddEventStoreClient("eventstore"); +``` + +Then, in your service, inject `EventStoreClient` and use it to interact with the EventStore API: + +```csharp +public class MyService(EventStoreClient eventStoreClient) +{ + // ... +} +``` + +## Additional documentation + +- https://github.com/EventStore/EventStore-Client-Dotnet +- https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-eventstore + +## Feedback & contributing + +https://github.com/CommunityToolkit/Aspire + diff --git a/src/CommunityToolkit.Aspire.Hosting.EventStore/CommunityToolkit.Aspire.Hosting.EventStore.csproj b/src/CommunityToolkit.Aspire.Hosting.EventStore/CommunityToolkit.Aspire.Hosting.EventStore.csproj new file mode 100644 index 00000000..39e36ef1 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.EventStore/CommunityToolkit.Aspire.Hosting.EventStore.csproj @@ -0,0 +1,20 @@ + + + + hosting eventstore + EventStore support for .NET Aspire. + + + + + + + + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs new file mode 100644 index 00000000..57e03794 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs @@ -0,0 +1,210 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Utils; +using CommunityToolkit.Aspire.Hosting.EventStore; +using HealthChecks.EventStore.gRPC; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding EventStore resources to the application model. +/// +public static class EventStoreBuilderExtensions +{ + private const string DataTargetFolder = "/var/lib/eventstore"; + private const string LogTargetFolder = "/var/log/eventstore"; + + /// + /// Adds an EventStore resource to the application model. A container is used for local development. + /// The default image is and the tag is . + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// The port on which the EventStore endpoint will be exposed. + /// A reference to the . + /// + /// + /// Add an EventStore container to the application model and reference it in a .NET project. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// var eventstore = builder.AddEventStore("eventstore"); + /// var api = builder.AddProject<Projects.Api>("api") + /// .WithReference(eventstore); + /// + /// builder.Build().Run(); + /// + /// + /// + public static IResourceBuilder AddEventStore( + this IDistributedApplicationBuilder builder, string name, int? port = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + + var eventStoreResource = new EventStoreResource(name); + + string? connectionString = null; + + builder.Eventing.Subscribe(eventStoreResource, async (@event, cancellationToken) => + { + connectionString = await eventStoreResource.ConnectionStringExpression + .GetValueAsync(cancellationToken) + .ConfigureAwait(false) + ?? throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{eventStoreResource.Name}' resource but the connection string was null."); + }); + + var healthCheckKey = $"{name}_check"; + builder.Services.AddHealthChecks() + .Add(new HealthCheckRegistration( + healthCheckKey, + sp => new EventStoreHealthCheck(connectionString!), + failureStatus: default, + tags: default, + timeout: default)); + + return builder + .AddResource(eventStoreResource) + .WithHttpEndpoint(port: port, targetPort: EventStoreResource.DefaultHttpPort, name: EventStoreResource.HttpEndpointName) + .WithImage(EventStoreContainerImageTags.Image, EventStoreContainerImageTags.Tag) + .WithImageRegistry(EventStoreContainerImageTags.Registry) + .WithEnvironment(ConfigureEventStoreContainer) + .WithHealthCheck(healthCheckKey); + } + + /// + /// Adds a named volume for the data folder to a EventStore container resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the application and resource names. + /// The . + /// + /// + /// Add an EventStore container to the application model and reference it in a .NET project. Additionally, in this + /// example a data volume is added to the container to allow data to be persisted across container restarts. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// var eventstore = builder.AddEventStore("eventstore") + /// .WithDataVolume(); + /// var api = builder.AddProject<Projects.Api>("api") + /// .WithReference(eventstore); + /// + /// builder.Build().Run(); + /// + /// + /// + public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null) + { + ArgumentNullException.ThrowIfNull(builder); + +#pragma warning disable CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), DataTargetFolder); +#pragma warning restore CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + + /// + /// Adds a bind mount for the data folder to a EventStore container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// The . + /// + /// + /// Add an EventStore container to the application model and reference it in a .NET project. Additionally, in this + /// example a bind mount is added to the container to allow data to be persisted across container restarts. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// var eventstore = builder.AddEventStore("eventstore") + /// .WithDataBindMount("./data/eventstore/data"); + /// var api = builder.AddProject<Projects.Api>("api") + /// .WithReference(eventstore); + /// + /// builder.Build().Run(); + /// + /// + /// + public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(source); + + return builder.WithBindMount(source, DataTargetFolder); + } + + /// + /// Adds a named volume for the log folder to a EventStore container resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the application and resource names. + /// The . + /// + /// + /// Add an EventStore container to the application model and reference it in a .NET project. Additionally, in this + /// example a log volume is added to the container to allow logs to be persisted across container restarts. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// var eventstore = builder.AddEventStore("eventstore") + /// .WithLogVolume(); + /// var api = builder.AddProject<Projects.Api>("api") + /// .WithReference(eventstore); + /// + /// builder.Build().Run(); + /// + /// + /// + public static IResourceBuilder WithLogVolume(this IResourceBuilder builder, string? name = null) + { + ArgumentNullException.ThrowIfNull(builder); + +#pragma warning disable CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "log"), LogTargetFolder); +#pragma warning restore CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + + /// + /// Adds a bind mount for the log folder to a EventStore container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// The . + /// + /// + /// Add an EventStore container to the application model and reference it in a .NET project. Additionally, in this + /// example a bind mount is added to the container to allow logs to be persisted across container restarts. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// var eventstore = builder.AddEventStore("eventstore") + /// .WithLogBindMount("./data/eventstore/logs"); + /// var api = builder.AddProject<Projects.Api>("api") + /// .WithReference(eventstore); + /// + /// builder.Build().Run(); + /// + /// + /// + public static IResourceBuilder WithLogBindMount(this IResourceBuilder builder, string source) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(source); + + return builder.WithBindMount(source, LogTargetFolder); + } + + private static void ConfigureEventStoreContainer(EnvironmentCallbackContext context) + { + context.EnvironmentVariables.Add("EVENTSTORE_CLUSTER_SIZE", "1"); + context.EnvironmentVariables.Add("EVENTSTORE_RUN_PROJECTIONS", "All"); + context.EnvironmentVariables.Add("EVENTSTORE_START_STANDARD_PROJECTIONS", "true"); + context.EnvironmentVariables.Add("EVENTSTORE_NODE_PORT", $"{EventStoreResource.DefaultHttpPort}"); + context.EnvironmentVariables.Add("EVENTSTORE_INSECURE", "true"); + context.EnvironmentVariables.Add("EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP", "true"); + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreContainerImageTags.cs new file mode 100644 index 00000000..056b4bbb --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreContainerImageTags.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace CommunityToolkit.Aspire.Hosting.EventStore; + +internal static class EventStoreContainerImageTags +{ + public const string Registry = "docker.io"; + public const string Image = "eventstore/eventstore"; + public const string Tag = "24.10"; +} diff --git a/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreResource.cs b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreResource.cs new file mode 100644 index 00000000..a0969d78 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreResource.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents an EventStore container. +/// +/// The name of the resource. +public class EventStoreResource(string name) : ContainerResource(name), IResourceWithConnectionString +{ + internal const string HttpEndpointName = "http"; + internal const int DefaultHttpPort = 2113; + + private EndpointReference? _primaryEndpoint; + + /// + /// Gets the primary endpoint for the EventStore server. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, HttpEndpointName); + + /// + /// Gets the connection string for the EventStore server. + /// + public ReferenceExpression ConnectionStringExpression => + ReferenceExpression.Create( + $"esdb://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}?tls=false"); +} diff --git a/src/CommunityToolkit.Aspire.Hosting.EventStore/PublicAPI.Shipped.txt b/src/CommunityToolkit.Aspire.Hosting.EventStore/PublicAPI.Shipped.txt new file mode 100644 index 00000000..8711a329 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.EventStore/PublicAPI.Shipped.txt @@ -0,0 +1,11 @@ +#nullable enable +Aspire.Hosting.ApplicationModel.EventStoreResource +Aspire.Hosting.ApplicationModel.EventStoreResource.EventStoreResource(string! name) -> void +Aspire.Hosting.ApplicationModel.EventStoreResource.PrimaryEndpoint.get -> Aspire.Hosting.ApplicationModel.EndpointReference! +Aspire.Hosting.ApplicationModel.EventStoreResource.ConnectionStringExpression.get -> Aspire.Hosting.ApplicationModel.ReferenceExpression! +Aspire.Hosting.EventStoreBuilderExtensions +static Aspire.Hosting.EventStoreBuilderExtensions.AddEventStore(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, int? port = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.EventStoreBuilderExtensions.WithDataVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.EventStoreBuilderExtensions.WithDataBindMount(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.EventStoreBuilderExtensions.WithLogVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.EventStoreBuilderExtensions.WithLogBindMount(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! diff --git a/src/CommunityToolkit.Aspire.Hosting.EventStore/PublicAPI.Unshipped.txt b/src/CommunityToolkit.Aspire.Hosting.EventStore/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..074c6ad1 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.EventStore/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +#nullable enable + diff --git a/src/CommunityToolkit.Aspire.Hosting.EventStore/README.md b/src/CommunityToolkit.Aspire.Hosting.EventStore/README.md new file mode 100644 index 00000000..18b48e4d --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.EventStore/README.md @@ -0,0 +1,37 @@ +# CommunityToolkit.Aspire.Hosting.EventStore library + +Provides extension methods and resource definitions for the .NET Aspire AppHost to support running [EventStore](https://www.eventstore.com) containers. + +## Getting Started + +### Install the package + +In your AppHost project, install the package using the following command: + +```dotnetcli +dotnet add package CommunityToolkit.Aspire.Hosting.EventStore +``` + +### Example usage + +Then, in the _Program.cs_ file of `AppHost`, add a EventStore resource and consume the connection using the following methods: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var eventstore = builder.AddEventStore("eventstore"); + +var myService = builder.AddProject() + .WithReference(eventstore); + +builder.Build().Run(); +``` + +## Additional Information + +https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-eventstore + +## Feedback & contributing + +https://github.com/CommunityToolkit/Aspire + diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs new file mode 100644 index 00000000..b53c9b48 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting; +using System.Net.Sockets; + +namespace CommunityToolkit.Aspire.Hosting.EventStore.Tests; + +public class AddEventStoreTests +{ + [Fact] + public async Task AddEventStoreContainerWithDefaultsAddsAnnotationMetadata() + { + var appBuilder = DistributedApplication.CreateBuilder(); + + var eventstore = appBuilder.AddEventStore("eventstore"); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var containerResource = Assert.Single(appModel.Resources.OfType()); + Assert.Equal("eventstore", containerResource.Name); + + var endpoints = containerResource.Annotations.OfType(); + Assert.Single(endpoints); + + var primaryEndpoint = Assert.Single(endpoints, e => e.Name == "http"); + Assert.Equal(2113, primaryEndpoint.TargetPort); + Assert.False(primaryEndpoint.IsExternal); + Assert.Equal("http", primaryEndpoint.Name); + Assert.Null(primaryEndpoint.Port); + Assert.Equal(ProtocolType.Tcp, primaryEndpoint.Protocol); + Assert.Equal("http", primaryEndpoint.Transport); + Assert.Equal("http", primaryEndpoint.UriScheme); + + var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); + Assert.Equal(EventStoreContainerImageTags.Tag, containerAnnotation.Tag); + Assert.Equal(EventStoreContainerImageTags.Image, containerAnnotation.Image); + Assert.Equal(EventStoreContainerImageTags.Registry, containerAnnotation.Registry); + + var config = await eventstore.Resource.GetEnvironmentVariableValuesAsync(); + + Assert.Equal(6, config.Count); + } + + [Fact] + public async Task EventStoreCreatesConnectionString() + { + var appBuilder = DistributedApplication.CreateBuilder(); + var eventstore = appBuilder + .AddEventStore("eventstore") + .WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 22113)); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var connectionStringResource = Assert.Single(appModel.Resources.OfType()) as IResourceWithConnectionString; + var connectionString = await connectionStringResource.GetConnectionStringAsync(); + + Assert.Equal("esdb://localhost:22113?tls=false", connectionString); + Assert.Equal("esdb://{eventstore.bindings.http.host}:{eventstore.bindings.http.port}?tls=false", connectionStringResource.ConnectionStringExpression.ValueExpression); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AppHostTests.cs new file mode 100644 index 00000000..854acce9 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AppHostTests.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.Common.Tests; +using CommunityToolkit.Aspire.Testing; +using FluentAssertions; +using Projects; +using System.Net.Http.Json; + +namespace CommunityToolkit.Aspire.Hosting.EventStore.Tests; + +[RequiresDocker] +public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture> +{ + [Fact] + public async Task ResourceStartsAndRespondsOk() + { + var resourceName = "eventstore"; + await fixture.ResourceNotificationService + .WaitForResourceAsync(resourceName, KnownResourceStates.Running) + .WaitAsync(TimeSpan.FromMinutes(1)); + + var httpClient = fixture.CreateHttpClient(resourceName); + + var response = await httpClient.GetAsync("/"); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + [Fact] + public async Task ApiServiceCreateAccount() + { + var resourceName = "apiservice"; + await fixture.ResourceNotificationService + .WaitForResourceAsync(resourceName, KnownResourceStates.Running) + .WaitAsync(TimeSpan.FromMinutes(1)); + + var httpClient = fixture.CreateHttpClient(resourceName); + + var createResponse = await httpClient.PostAsJsonAsync("/account/create", new { }); + createResponse.StatusCode.Should().Be(HttpStatusCode.Created); + + var location = createResponse.Headers.Location; + + var getResponse = await httpClient.GetAsync(location); + getResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + var account = await getResponse.Content.ReadFromJsonAsync(); + Assert.NotNull(account); + Assert.Equal("John Doe", account.Name); + Assert.Equal(100, account.Balance); + } + + [Fact] + public async Task ApiServiceCreateAccountAndDeposit() + { + var resourceName = "apiservice"; + await fixture.ResourceNotificationService + .WaitForResourceAsync(resourceName, KnownResourceStates.Running) + .WaitAsync(TimeSpan.FromMinutes(1)); + + var httpClient = fixture.CreateHttpClient(resourceName); + + var createResponse = await httpClient.PostAsJsonAsync("/account/create", new { }); + createResponse.StatusCode.Should().Be(HttpStatusCode.Created); + + var location = createResponse.Headers.Location; + + var depositResponse = await httpClient.PostAsJsonAsync($"{location!}/deposit", new { Amount = 50 }); + depositResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + var getResponse = await httpClient.GetAsync(location); + getResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + var account = await getResponse.Content.ReadFromJsonAsync(); + Assert.NotNull(account); + Assert.Equal("John Doe", account.Name); + Assert.Equal(150, account.Balance); + } + + [Fact] + public async Task ApiServiceCreateAccountAndWithdraw() + { + var resourceName = "apiservice"; + await fixture.ResourceNotificationService + .WaitForResourceAsync(resourceName, KnownResourceStates.Running) + .WaitAsync(TimeSpan.FromMinutes(1)); + + var httpClient = fixture.CreateHttpClient(resourceName); + + var createResponse = await httpClient.PostAsJsonAsync("/account/create", new { }); + createResponse.StatusCode.Should().Be(HttpStatusCode.Created); + + var location = createResponse.Headers.Location; + + var depositResponse = await httpClient.PostAsJsonAsync($"{location!}/withdraw", new { Amount = 90 }); + depositResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + var getResponse = await httpClient.GetAsync(location); + getResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + var account = await getResponse.Content.ReadFromJsonAsync(); + Assert.NotNull(account); + Assert.Equal("John Doe", account.Name); + Assert.Equal(10, account.Balance); + } + + public record AccountDto(Guid Id, string Name, decimal Balance); +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests.csproj new file mode 100644 index 00000000..b982e71b --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests.csproj @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStorePublicApiTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStorePublicApiTests.cs new file mode 100644 index 00000000..0c0e0c07 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStorePublicApiTests.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.EventStore.Tests; + +public class EventStorePublicApiTests +{ + [Fact] + public void AddEventStoreShouldThrowWhenBuilderIsNull() + { + IDistributedApplicationBuilder builder = null!; + const string name = "eventstore"; + + var action = () => builder.AddEventStore(name); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } + + [Fact] + public void AddEventStoreShouldThrowWhenNameIsNull() + { + var builder = new DistributedApplicationBuilder([]); + string name = null!; + + var action = () => builder.AddEventStore(name); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(name), exception.ParamName); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void WithDataShouldThrowWhenBuilderIsNull(bool useVolume) + { + IResourceBuilder builder = null!; + + Func>? action = null; + + if (useVolume) + { + action = () => builder.WithDataVolume(); + } + else + { + const string source = "/data"; + + action = () => builder.WithDataBindMount(source); + } + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } + + [Fact] + public void WithDataBindMountShouldThrowWhenSourceIsNull() + { + var builder = new DistributedApplicationBuilder([]); + var resourceBuilder = builder.AddEventStore("eventstore"); + + string source = null!; + + var action = () => resourceBuilder.WithDataBindMount(source); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(source), exception.ParamName); + } + + [Fact] + public void EventStoreResourceCtorShouldThrowWhenNameIsNull() + { + var builder = new DistributedApplicationBuilder([]); + const string name = null!; + + var action = () => new EventStoreResource(name); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(name), exception.ParamName); + } +} From 5da0502c5b86dc265e58bf5bb7c3b8829314269d Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Fri, 22 Nov 2024 09:35:19 +1100 Subject: [PATCH 02/26] Update CODEOWNERS --- CODEOWNERS | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index 428aa766..fc7bd2cf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -41,3 +41,11 @@ /examples/rust/_ @Alirexaa /src/CommunityToolkit.Aspire.Hosting.Rust/_ @Alirexaa /tests/CommunityToolkit.Aspire.Hosting.Rust/_ @Alirexaa + +# CommunityToolkit.Aspire.EventStore +# CommunityToolkit.Aspire.Hosting.EventStore + +/examples/eventstore/_ @fredimachado +/src/CommunityToolkit.Aspire.Hosting.EventStore/_ @fredimachado +/tests/CommunityToolkit.Aspire.Hosting.EventStore/_ @fredimachado +/src/CommunityToolkit.Aspire.EventStore/_ @fredimachado From 259b29b69957d76fa95caab2b5b7b82c3ae2e6da Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Fri, 22 Nov 2024 18:00:02 +1100 Subject: [PATCH 03/26] Address PR feedback --- .../EventStoreBuilderExtensions.cs | 3 +- .../README.md | 6 +- .../AddEventStoreTests.cs | 6 + .../AppHostTests.cs | 11 +- .../EventStorePublicApiTests.cs | 156 +++++++++++++++++- 5 files changed, 171 insertions(+), 11 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs index 57e03794..a2ebfa7b 100644 --- a/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs @@ -40,8 +40,7 @@ public static class EventStoreBuilderExtensions /// /// /// - public static IResourceBuilder AddEventStore( - this IDistributedApplicationBuilder builder, string name, int? port = null) + public static IResourceBuilder AddEventStore(this IDistributedApplicationBuilder builder, [ResourceName] string name, int? port = null) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(name); diff --git a/src/CommunityToolkit.Aspire.Hosting.EventStore/README.md b/src/CommunityToolkit.Aspire.Hosting.EventStore/README.md index 18b48e4d..518b6509 100644 --- a/src/CommunityToolkit.Aspire.Hosting.EventStore/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.EventStore/README.md @@ -1,12 +1,12 @@ # CommunityToolkit.Aspire.Hosting.EventStore library -Provides extension methods and resource definitions for the .NET Aspire AppHost to support running [EventStore](https://www.eventstore.com) containers. +Provides extension methods and resource definitions for the .NET Aspire app host to support running [EventStore](https://www.eventstore.com) containers. ## Getting Started ### Install the package -In your AppHost project, install the package using the following command: +In your app host project, install the package using the following command: ```dotnetcli dotnet add package CommunityToolkit.Aspire.Hosting.EventStore @@ -14,7 +14,7 @@ dotnet add package CommunityToolkit.Aspire.Hosting.EventStore ### Example usage -Then, in the _Program.cs_ file of `AppHost`, add a EventStore resource and consume the connection using the following methods: +Then, in the _Program.cs_ file of app host, add a EventStore resource and consume the connection using the following methods: ```csharp var builder = DistributedApplication.CreateBuilder(args); diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs index b53c9b48..2846ca7e 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs @@ -42,6 +42,12 @@ public async Task AddEventStoreContainerWithDefaultsAddsAnnotationMetadata() var config = await eventstore.Resource.GetEnvironmentVariableValuesAsync(); Assert.Equal(6, config.Count); + Assert.Equal("1", config["EVENTSTORE_CLUSTER_SIZE"]); + Assert.Equal("All", config["EVENTSTORE_RUN_PROJECTIONS"]); + Assert.Equal("true", config["EVENTSTORE_START_STANDARD_PROJECTIONS"]); + Assert.Equal($"{EventStoreResource.DefaultHttpPort}", config["EVENTSTORE_NODE_PORT"]); + Assert.Equal("true", config["EVENTSTORE_INSECURE"]); + Assert.Equal("true", config["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"]); } [Fact] diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AppHostTests.cs index 854acce9..8b9a9a4d 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AppHostTests.cs @@ -17,7 +17,7 @@ public async Task ResourceStartsAndRespondsOk() { var resourceName = "eventstore"; await fixture.ResourceNotificationService - .WaitForResourceAsync(resourceName, KnownResourceStates.Running) + .WaitForResourceHealthyAsync(resourceName) .WaitAsync(TimeSpan.FromMinutes(1)); var httpClient = fixture.CreateHttpClient(resourceName); @@ -32,7 +32,10 @@ public async Task ApiServiceCreateAccount() { var resourceName = "apiservice"; await fixture.ResourceNotificationService - .WaitForResourceAsync(resourceName, KnownResourceStates.Running) + .WaitForResourceHealthyAsync("eventstore") + .WaitAsync(TimeSpan.FromMinutes(1)); + await fixture.ResourceNotificationService + .WaitForResourceHealthyAsync(resourceName) .WaitAsync(TimeSpan.FromMinutes(1)); var httpClient = fixture.CreateHttpClient(resourceName); @@ -56,7 +59,7 @@ public async Task ApiServiceCreateAccountAndDeposit() { var resourceName = "apiservice"; await fixture.ResourceNotificationService - .WaitForResourceAsync(resourceName, KnownResourceStates.Running) + .WaitForResourceHealthyAsync(resourceName) .WaitAsync(TimeSpan.FromMinutes(1)); var httpClient = fixture.CreateHttpClient(resourceName); @@ -83,7 +86,7 @@ public async Task ApiServiceCreateAccountAndWithdraw() { var resourceName = "apiservice"; await fixture.ResourceNotificationService - .WaitForResourceAsync(resourceName, KnownResourceStates.Running) + .WaitForResourceHealthyAsync(resourceName) .WaitAsync(TimeSpan.FromMinutes(1)); var httpClient = fixture.CreateHttpClient(resourceName); diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStorePublicApiTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStorePublicApiTests.cs index 0c0e0c07..aa63951a 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStorePublicApiTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStorePublicApiTests.cs @@ -59,16 +59,168 @@ public void WithDataShouldThrowWhenBuilderIsNull(bool useVolume) public void WithDataBindMountShouldThrowWhenSourceIsNull() { var builder = new DistributedApplicationBuilder([]); - var resourceBuilder = builder.AddEventStore("eventstore"); + var eventstore = builder.AddEventStore("eventstore"); string source = null!; - var action = () => resourceBuilder.WithDataBindMount(source); + var action = () => eventstore.WithDataBindMount(source); var exception = Assert.Throws(action); Assert.Equal(nameof(source), exception.ParamName); } + [Fact] + public void WithDataVolumeShouldAddMountAnnotation() + { + var builder = new DistributedApplicationBuilder([]); + var eventstore = builder.AddEventStore("eventstore") + .WithDataVolume(name: null); + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + Assert.Equal("eventstore", resource.Name); + + Assert.True(resource.TryGetLastAnnotation(out ContainerMountAnnotation? mountAnnotation)); + Assert.EndsWith("-data", mountAnnotation.Source); + Assert.Equal("/var/lib/eventstore", mountAnnotation.Target); + } + + [Fact] + public void WithNamedDataVolumeShouldAddMountAnnotation() + { + var builder = new DistributedApplicationBuilder([]); + var eventstore = builder.AddEventStore("eventstore") + .WithDataVolume("mydata"); + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + Assert.Equal("eventstore", resource.Name); + + Assert.True(resource.TryGetLastAnnotation(out ContainerMountAnnotation? mountAnnotation)); + Assert.Equal("mydata", mountAnnotation.Source); + Assert.Equal("/var/lib/eventstore", mountAnnotation.Target); + } + + [Fact] + public void WithDataBindMountShouldAddMountAnnotation() + { + var builder = new DistributedApplicationBuilder([]); + var eventstore = builder.AddEventStore("eventstore") + .WithDataBindMount("./mydata"); + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + Assert.Equal("eventstore", resource.Name); + + Assert.True(resource.TryGetLastAnnotation(out ContainerMountAnnotation? mountAnnotation)); + Assert.EndsWith("mydata", mountAnnotation.Source); + Assert.Equal("/var/lib/eventstore", mountAnnotation.Target); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void WithLogShouldThrowWhenBuilderIsNull(bool useVolume) + { + IResourceBuilder builder = null!; + + Func>? action = null; + + if (useVolume) + { + action = () => builder.WithLogVolume(); + } + else + { + const string source = "/data"; + + action = () => builder.WithLogBindMount(source); + } + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } + + [Fact] + public void WithLogBindMountShouldThrowWhenSourceIsNull() + { + var builder = new DistributedApplicationBuilder([]); + var eventstore = builder.AddEventStore("eventstore"); + + string source = null!; + + var action = () => eventstore.WithLogBindMount(source); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(source), exception.ParamName); + } + + [Fact] + public void WithLogVolumeShouldAddMountAnnotation() + { + var builder = new DistributedApplicationBuilder([]); + var eventstore = builder.AddEventStore("eventstore") + .WithLogVolume(name: null); + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + Assert.Equal("eventstore", resource.Name); + + Assert.True(resource.TryGetLastAnnotation(out ContainerMountAnnotation? mountAnnotation)); + Assert.EndsWith("-log", mountAnnotation.Source); + Assert.Equal("/var/log/eventstore", mountAnnotation.Target); + } + + [Fact] + public void WithNamedLogVolumeShouldAddMountAnnotation() + { + var builder = new DistributedApplicationBuilder([]); + var eventstore = builder.AddEventStore("eventstore") + .WithLogVolume("eventstore-logs"); + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + Assert.Equal("eventstore", resource.Name); + + Assert.True(resource.TryGetLastAnnotation(out ContainerMountAnnotation? mountAnnotation)); + Assert.Equal("eventstore-logs", mountAnnotation.Source); + Assert.Equal("/var/log/eventstore", mountAnnotation.Target); + } + + [Fact] + public void WithLogBindMountShouldAddMountAnnotation() + { + var builder = new DistributedApplicationBuilder([]); + var eventstore = builder.AddEventStore("eventstore") + .WithLogBindMount("./mylogs"); + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + Assert.Equal("eventstore", resource.Name); + + Assert.True(resource.TryGetLastAnnotation(out ContainerMountAnnotation? mountAnnotation)); + Assert.EndsWith("mylogs", mountAnnotation.Source); + Assert.Equal("/var/log/eventstore", mountAnnotation.Target); + } + [Fact] public void EventStoreResourceCtorShouldThrowWhenNameIsNull() { From bb12675639b7d59a6f95353761f22bebcfcaa9b5 Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Fri, 22 Nov 2024 23:05:29 +1100 Subject: [PATCH 04/26] Add client integration tests --- CODEOWNERS | 3 +- CommunityToolkit.Aspire.sln | 7 ++ .../AspireEventStoreExtensions.cs | 34 ++++--- .../AspireEventStoreClientExtensionsTest.cs | 89 ++++++++++++++++++ ...nityToolkit.Aspire.EventStore.Tests.csproj | 12 +++ .../ConfigurationTests.cs | 19 ++++ .../ConformanceTests.cs | 92 +++++++++++++++++++ .../EventStoreClientPublicApiTests.cs | 88 ++++++++++++++++++ .../EventStoreContainerFixture.cs | 49 ++++++++++ 9 files changed, 380 insertions(+), 13 deletions(-) create mode 100644 tests/CommunityToolkit.Aspire.EventStore.Tests/AspireEventStoreClientExtensionsTest.cs create mode 100644 tests/CommunityToolkit.Aspire.EventStore.Tests/CommunityToolkit.Aspire.EventStore.Tests.csproj create mode 100644 tests/CommunityToolkit.Aspire.EventStore.Tests/ConfigurationTests.cs create mode 100644 tests/CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs create mode 100644 tests/CommunityToolkit.Aspire.EventStore.Tests/EventStoreClientPublicApiTests.cs create mode 100644 tests/CommunityToolkit.Aspire.EventStore.Tests/EventStoreContainerFixture.cs diff --git a/CODEOWNERS b/CODEOWNERS index fc7bd2cf..f6f2c1a9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -47,5 +47,6 @@ /examples/eventstore/_ @fredimachado /src/CommunityToolkit.Aspire.Hosting.EventStore/_ @fredimachado -/tests/CommunityToolkit.Aspire.Hosting.EventStore/_ @fredimachado +/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/_ @fredimachado /src/CommunityToolkit.Aspire.EventStore/_ @fredimachado +/tests/CommunityToolkit.Aspire.EventStore.Tests/_ @fredimachado diff --git a/CommunityToolkit.Aspire.sln b/CommunityToolkit.Aspire.sln index 5bf544f8..b8269d5f 100644 --- a/CommunityToolkit.Aspire.sln +++ b/CommunityToolkit.Aspire.sln @@ -159,6 +159,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hos EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.EventStore.ApiService", "examples\eventstore\CommunityToolkit.Aspire.Hosting.EventStore.ApiService\CommunityToolkit.Aspire.Hosting.EventStore.ApiService.csproj", "{019D6506-9D68-41AD-A7A1-A27B2FFE1253}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.EventStore.Tests", "tests\CommunityToolkit.Aspire.EventStore.Tests\CommunityToolkit.Aspire.EventStore.Tests.csproj", "{C696480B-C2E0-4ACA-BD5E-A62BF8558024}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -409,6 +411,10 @@ Global {019D6506-9D68-41AD-A7A1-A27B2FFE1253}.Debug|Any CPU.Build.0 = Debug|Any CPU {019D6506-9D68-41AD-A7A1-A27B2FFE1253}.Release|Any CPU.ActiveCfg = Release|Any CPU {019D6506-9D68-41AD-A7A1-A27B2FFE1253}.Release|Any CPU.Build.0 = Release|Any CPU + {C696480B-C2E0-4ACA-BD5E-A62BF8558024}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C696480B-C2E0-4ACA-BD5E-A62BF8558024}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C696480B-C2E0-4ACA-BD5E-A62BF8558024}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C696480B-C2E0-4ACA-BD5E-A62BF8558024}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -489,6 +495,7 @@ Global {ED3E5B89-091C-4A0E-9A2B-946CA1A11557} = {114DDF07-489A-419B-BE76-E5A289F12791} {0A4E5B43-155A-4FDA-A50F-0B86CF1705E7} = {114DDF07-489A-419B-BE76-E5A289F12791} {019D6506-9D68-41AD-A7A1-A27B2FFE1253} = {114DDF07-489A-419B-BE76-E5A289F12791} + {C696480B-C2E0-4ACA-BD5E-A62BF8558024} = {899F0713-7FC6-4750-BAFC-AC650B35B453} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {08B1D4B8-D2C5-4A64-BB8B-E1A2B29525F0} diff --git a/src/CommunityToolkit.Aspire.EventStore/AspireEventStoreExtensions.cs b/src/CommunityToolkit.Aspire.EventStore/AspireEventStoreExtensions.cs index f227fcb0..0097cb0f 100644 --- a/src/CommunityToolkit.Aspire.EventStore/AspireEventStoreExtensions.cs +++ b/src/CommunityToolkit.Aspire.EventStore/AspireEventStoreExtensions.cs @@ -32,7 +32,7 @@ public static void AddEventStoreClient( { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNullOrEmpty(connectionName); - AddEventStoreClient(builder, configureSettings, connectionName, serviceKey: null); + AddEventStoreClient(builder, DefaultConfigSectionName, configureSettings, connectionName, serviceKey: null); } /// @@ -48,23 +48,20 @@ public static void AddKeyedEventStoreClient( { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNullOrEmpty(name); - AddEventStoreClient(builder, configureSettings, connectionName: name, serviceKey: name); + AddEventStoreClient(builder, $"{DefaultConfigSectionName}:{name}", configureSettings, connectionName: name, serviceKey: name); } private static void AddEventStoreClient( this IHostApplicationBuilder builder, + string configurationSectionName, Action? configureSettings, string connectionName, string? serviceKey) { ArgumentNullException.ThrowIfNull(builder); - var configSection = builder.Configuration.GetSection(DefaultConfigSectionName); - var namedConfigSection = configSection.GetSection(connectionName); - var settings = new EventStoreSettings(); - configSection.Bind(settings); - namedConfigSection.Bind(settings); + builder.Configuration.GetSection(configurationSectionName).Bind(settings); if (builder.Configuration.GetConnectionString(connectionName) is string connectionString) { @@ -73,16 +70,13 @@ private static void AddEventStoreClient( configureSettings?.Invoke(settings); - var eventStoreClientSettings = EventStoreClientSettings.Create(settings.ConnectionString!); - var eventStoreClient = new EventStoreClient(eventStoreClientSettings); - if (serviceKey is null) { - builder.Services.AddSingleton(eventStoreClient); + builder.Services.AddSingleton(ConfigureEventStoreClient); } else { - builder.Services.AddKeyedSingleton(serviceKey, (sp, key) => eventStoreClient); + builder.Services.AddKeyedSingleton(serviceKey, (sp, key) => ConfigureEventStoreClient(sp)); } if (!settings.DisableTracing) @@ -102,5 +96,21 @@ private static void AddEventStoreClient( tags: default, timeout: default)); } + + EventStoreClient ConfigureEventStoreClient(IServiceProvider serviceProvider) + { + if (settings.ConnectionString is not null) + { + var eventStoreClientSettings = EventStoreClientSettings.Create(settings.ConnectionString!); + return new EventStoreClient(eventStoreClientSettings); + } + else + { + throw new InvalidOperationException( + $"An EventStore could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}' or either " + + $"{nameof(settings.ConnectionString)} must be provided " + + $"in the '{configurationSectionName}' configuration section."); + } + } } } diff --git a/tests/CommunityToolkit.Aspire.EventStore.Tests/AspireEventStoreClientExtensionsTest.cs b/tests/CommunityToolkit.Aspire.EventStore.Tests/AspireEventStoreClientExtensionsTest.cs new file mode 100644 index 00000000..adf1307b --- /dev/null +++ b/tests/CommunityToolkit.Aspire.EventStore.Tests/AspireEventStoreClientExtensionsTest.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.Common.Tests; +using EventStore.Client; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; + +namespace CommunityToolkit.Aspire.EventStore.Tests; + +public class AspireEventStoreClientExtensionsTest(EventStoreContainerFixture containerFixture) : IClassFixture +{ + private const string DefaultConnectionName = "eventstore"; + + private string DefaultConnectionString => + RequiresDockerAttribute.IsSupported ? containerFixture.GetConnectionString() : "esdb://localhost:2113?tls=false"; + + [Theory] + [InlineData(true)] + [InlineData(false)] + [RequiresDocker] + public async Task AddEventStoreClient_HealthCheckShouldBeRegisteredWhenEnabled(bool useKeyed) + { + var key = DefaultConnectionName; + + var builder = CreateBuilder(DefaultConnectionString); + + if (useKeyed) + { + builder.AddKeyedEventStoreClient(key, settings => + { + settings.DisableHealthChecks = false; + }); + } + else + { + builder.AddEventStoreClient(DefaultConnectionName, settings => + { + settings.DisableHealthChecks = false; + }); + } + + using var host = builder.Build(); + + var healthCheckService = host.Services.GetRequiredService(); + + var healthCheckReport = await healthCheckService.CheckHealthAsync(); + + var healthCheckName = useKeyed ? $"EventStore.Client_{key}" : "EventStore.Client"; + + Assert.Contains(healthCheckReport.Entries, x => x.Key == healthCheckName); + } + + [Fact] + public void CanAddMultipleKeyedServices() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:eventstore1", "esdb://localhost:22113?tls=false"), + new KeyValuePair("ConnectionStrings:eventstore2", "esdb://localhost:22114?tls=false"), + new KeyValuePair("ConnectionStrings:eventstore3", "esdb://localhost:22115?tls=false"), + ]); + + builder.AddEventStoreClient("eventstore1"); + builder.AddKeyedEventStoreClient("eventstore2"); + builder.AddKeyedEventStoreClient("eventstore3"); + + using var host = builder.Build(); + + var client1 = host.Services.GetRequiredService(); + var client2 = host.Services.GetRequiredKeyedService("eventstore2"); + var client3 = host.Services.GetRequiredKeyedService("eventstore3"); + + Assert.NotSame(client1, client2); + Assert.NotSame(client1, client3); + Assert.NotSame(client2, client3); + } + + private static HostApplicationBuilder CreateBuilder(string connectionString) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair($"ConnectionStrings:{DefaultConnectionName}", connectionString) + ]); + return builder; + } +} diff --git a/tests/CommunityToolkit.Aspire.EventStore.Tests/CommunityToolkit.Aspire.EventStore.Tests.csproj b/tests/CommunityToolkit.Aspire.EventStore.Tests/CommunityToolkit.Aspire.EventStore.Tests.csproj new file mode 100644 index 00000000..fd0b3c3b --- /dev/null +++ b/tests/CommunityToolkit.Aspire.EventStore.Tests/CommunityToolkit.Aspire.EventStore.Tests.csproj @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.EventStore.Tests/ConfigurationTests.cs b/tests/CommunityToolkit.Aspire.EventStore.Tests/ConfigurationTests.cs new file mode 100644 index 00000000..e799bc18 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.EventStore.Tests/ConfigurationTests.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace CommunityToolkit.Aspire.EventStore.Tests; + +public class ConfigurationTests +{ + [Fact] + public void ConnectionStringIsNullByDefault() => + Assert.Null(new EventStoreSettings().ConnectionString); + + [Fact] + public void HealthChecksEnabledByDefault() => + Assert.False(new EventStoreSettings().DisableHealthChecks); + + [Fact] + public void DisableTracingIsFalseByDefault() => + Assert.False(new EventStoreSettings().DisableTracing); +} diff --git a/tests/CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs b/tests/CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs new file mode 100644 index 00000000..ec263722 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.Common.Tests; +using Aspire.Components.ConformanceTests; +using EventStore.Client; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace CommunityToolkit.Aspire.EventStore.Tests; + +public class ConformanceTests(EventStoreContainerFixture containerFixture) : ConformanceTests, IClassFixture +{ + private readonly EventStoreContainerFixture _containerFixture = containerFixture; + + protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton; + + protected override string ActivitySourceName => string.Empty; + + protected override string[] RequiredLogCategories => []; + + protected override bool CanConnectToServer => RequiresDockerAttribute.IsSupported; + + protected override bool SupportsKeyedRegistrations => true; + + protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) + { + var connectionString = RequiresDockerAttribute.IsSupported + ? $"{_containerFixture.GetConnectionString()}" + : "esdb://localhost:22113?tls=false"; + + configuration.AddInMemoryCollection( + [ + new KeyValuePair($"Aspire:EventStore:Client:ConnectionString", $"{connectionString}"), + new KeyValuePair($"ConnectionStrings:{key}", $"{connectionString}") + ]); + } + + protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) + { + if (key is null) + { + builder.AddEventStoreClient("eventstore", configureSettings: configure); + } + else + { + builder.AddKeyedEventStoreClient(key, configureSettings: configure); + } + } + + protected override string ValidJsonConfig => """ + { + "Aspire": { + "EventStore": { + "Client": { + "ConnectionString": "esdb://localhost:22113?tls=false", + "DisableHealthChecks": "false" + } + } + } + } + """; + + protected override (string json, string error)[] InvalidJsonToErrorMessage => new[] + { + ("""{"Aspire": { "EventStore":{ "Client": { "ConnectionString": 3 }}}}""", "Value is \"integer\" but should be \"string\"") + }; + + protected override void SetHealthCheck(EventStoreSettings options, bool enabled) + { + options.DisableHealthChecks = !enabled; + } + + protected override void SetMetrics(EventStoreSettings options, bool enabled) + { + throw new NotImplementedException(); + } + + protected override void SetTracing(EventStoreSettings options, bool enabled) + { + throw new NotImplementedException(); + } + + protected override void TriggerActivity(EventStoreClient service) + { + using var source = new CancellationTokenSource(100); + + var readResult = service.ReadAllAsync(direction: Direction.Backwards, position: Position.End, maxCount: 1); + + readResult.Messages.ToArrayAsync().GetAwaiter().GetResult(); + } +} diff --git a/tests/CommunityToolkit.Aspire.EventStore.Tests/EventStoreClientPublicApiTests.cs b/tests/CommunityToolkit.Aspire.EventStore.Tests/EventStoreClientPublicApiTests.cs new file mode 100644 index 00000000..19fe11f0 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.EventStore.Tests/EventStoreClientPublicApiTests.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace CommunityToolkit.Aspire.EventStore.Tests; + +public class EventStoreClientPublicApiTests +{ + [Fact] + public void AddEventStoreClientShouldThrowWhenBuilderIsNull() + { + IHostApplicationBuilder builder = null!; + + var connectionName = "eventstore"; + + var action = () => builder.AddEventStoreClient(connectionName); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } + + [Fact] + public void AddEventStoreClientShouldThrowWhenNameIsNull() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + string connectionName = null!; + + var action = () => builder.AddEventStoreClient(connectionName); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(connectionName), exception.ParamName); + } + + [Fact] + public void AddEventStoreClientShouldThrowWhenNameIsEmpty() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + string connectionName = ""; + + var action = () => builder.AddEventStoreClient(connectionName); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(connectionName), exception.ParamName); + } + + [Fact] + public void AddKeyedEventStoreClientShouldThrowWhenBuilderIsNull() + { + IHostApplicationBuilder builder = null!; + + var connectionName = "eventstore"; + + var action = () => builder.AddKeyedEventStoreClient(connectionName); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } + + [Fact] + public void AddKeyedEventStoreClientShouldThrowWhenNameIsNull() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + string name = null!; + + var action = () => builder.AddKeyedEventStoreClient(name); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(name), exception.ParamName); + } + + [Fact] + public void AddKeyedEventStoreClientShouldThrowWhenNameIsEmpty() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + string name = ""; + + var action = () => builder.AddKeyedEventStoreClient(name); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(name), exception.ParamName); + } +} diff --git a/tests/CommunityToolkit.Aspire.EventStore.Tests/EventStoreContainerFixture.cs b/tests/CommunityToolkit.Aspire.EventStore.Tests/EventStoreContainerFixture.cs new file mode 100644 index 00000000..e8f07a8d --- /dev/null +++ b/tests/CommunityToolkit.Aspire.EventStore.Tests/EventStoreContainerFixture.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.Common.Tests; +using CommunityToolkit.Aspire.Hosting.EventStore; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; + +namespace CommunityToolkit.Aspire.EventStore.Tests; + +public sealed class EventStoreContainerFixture : IAsyncLifetime +{ + public IContainer? Container { get; private set; } + + public string GetConnectionString() + { + if (Container is null) + { + throw new InvalidOperationException("The test container was not initialized."); + } + var endpoint = new UriBuilder("esdb", Container.Hostname, Container.GetMappedPublicPort(2113)).ToString(); + return $"{endpoint}?tls=false"; + } + + public async Task InitializeAsync() + { + if (RequiresDockerAttribute.IsSupported) + { + Container = new ContainerBuilder() + .WithImage($"{EventStoreContainerImageTags.Registry}/{EventStoreContainerImageTags.Image}:{EventStoreContainerImageTags.Tag}") + .WithPortBinding(2113, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(2113))) + .WithEnvironment("EVENTSTORE_CLUSTER_SIZE", "1") + .WithEnvironment("EVENTSTORE_NODE_PORT", "2113") + .WithEnvironment("EVENTSTORE_INSECURE", "true") + .Build(); + + await Container.StartAsync(); + } + } + + public async Task DisposeAsync() + { + if (Container is not null) + { + await Container.DisposeAsync(); + } + } +} From 6843bd977ba73d78f881cceb6317b8b6d54a7a23 Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Sun, 24 Nov 2024 10:06:31 +1100 Subject: [PATCH 05/26] Update AddEventStoreTests.cs --- .../AddEventStoreTests.cs | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs index 2846ca7e..bcb8426d 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs @@ -26,7 +26,7 @@ public async Task AddEventStoreContainerWithDefaultsAddsAnnotationMetadata() Assert.Single(endpoints); var primaryEndpoint = Assert.Single(endpoints, e => e.Name == "http"); - Assert.Equal(2113, primaryEndpoint.TargetPort); + Assert.Equal(EventStoreResource.DefaultHttpPort, primaryEndpoint.TargetPort); Assert.False(primaryEndpoint.IsExternal); Assert.Equal("http", primaryEndpoint.Name); Assert.Null(primaryEndpoint.Port); @@ -41,13 +41,37 @@ public async Task AddEventStoreContainerWithDefaultsAddsAnnotationMetadata() var config = await eventstore.Resource.GetEnvironmentVariableValuesAsync(); - Assert.Equal(6, config.Count); - Assert.Equal("1", config["EVENTSTORE_CLUSTER_SIZE"]); - Assert.Equal("All", config["EVENTSTORE_RUN_PROJECTIONS"]); - Assert.Equal("true", config["EVENTSTORE_START_STANDARD_PROJECTIONS"]); - Assert.Equal($"{EventStoreResource.DefaultHttpPort}", config["EVENTSTORE_NODE_PORT"]); - Assert.Equal("true", config["EVENTSTORE_INSECURE"]); - Assert.Equal("true", config["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"]); + Assert.Collection(config, + env => + { + Assert.Equal("EVENTSTORE_CLUSTER_SIZE", env.Key); + Assert.Equal("1", env.Value); + }, + env => + { + Assert.Equal("EVENTSTORE_RUN_PROJECTIONS", env.Key); + Assert.Equal("All", env.Value); + }, + env => + { + Assert.Equal("EVENTSTORE_START_STANDARD_PROJECTIONS", env.Key); + Assert.Equal("true", env.Value); + }, + env => + { + Assert.Equal("EVENTSTORE_NODE_PORT", env.Key); + Assert.Equal($"{EventStoreResource.DefaultHttpPort}", env.Value); + }, + ext => + { + Assert.Equal("EVENTSTORE_INSECURE", ext.Key); + Assert.Equal("true", ext.Value); + }, + ext => + { + Assert.Equal("EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP", ext.Key); + Assert.Equal("true", ext.Value); + }); } [Fact] From a8b80666098b3aa767139760aca63d1a1807dc8a Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Mon, 25 Nov 2024 18:39:39 +1100 Subject: [PATCH 06/26] Add HealthCheckTimeout setting --- .../AspireEventStoreExtensions.cs | 2 +- .../EventStoreSettings.cs | 8 ++++++++ .../PublicAPI.Shipped.txt | 4 +++- .../ConfigurationTests.cs | 4 ++++ .../ConformanceTests.cs | 1 + 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Aspire.EventStore/AspireEventStoreExtensions.cs b/src/CommunityToolkit.Aspire.EventStore/AspireEventStoreExtensions.cs index 0097cb0f..29bcef4e 100644 --- a/src/CommunityToolkit.Aspire.EventStore/AspireEventStoreExtensions.cs +++ b/src/CommunityToolkit.Aspire.EventStore/AspireEventStoreExtensions.cs @@ -94,7 +94,7 @@ private static void AddEventStoreClient( sp => new EventStoreHealthCheck(settings.ConnectionString!), failureStatus: default, tags: default, - timeout: default)); + timeout: settings.HealthCheckTimeout)); } EventStoreClient ConfigureEventStoreClient(IServiceProvider serviceProvider) diff --git a/src/CommunityToolkit.Aspire.EventStore/EventStoreSettings.cs b/src/CommunityToolkit.Aspire.EventStore/EventStoreSettings.cs index 0bc199b6..3c341e7e 100644 --- a/src/CommunityToolkit.Aspire.EventStore/EventStoreSettings.cs +++ b/src/CommunityToolkit.Aspire.EventStore/EventStoreSettings.cs @@ -20,6 +20,14 @@ public sealed class EventStoreSettings /// The default value is . /// public bool DisableHealthChecks { get; set; } + + /// + /// Gets or sets the timeout duration for the health check. + /// + /// + /// The default value is . + /// + public TimeSpan? HealthCheckTimeout { get; set; } /// /// Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is disabled or not. diff --git a/src/CommunityToolkit.Aspire.EventStore/PublicAPI.Shipped.txt b/src/CommunityToolkit.Aspire.EventStore/PublicAPI.Shipped.txt index 9aabdbb6..897e7c3c 100644 --- a/src/CommunityToolkit.Aspire.EventStore/PublicAPI.Shipped.txt +++ b/src/CommunityToolkit.Aspire.EventStore/PublicAPI.Shipped.txt @@ -1,10 +1,12 @@ #nullable enable CommunityToolkit.Aspire.EventStore.EventStoreSettings +CommunityToolkit.Aspire.EventStore.EventStoreSettings.EventStoreSettings() -> void CommunityToolkit.Aspire.EventStore.EventStoreSettings.ConnectionString.get -> string? CommunityToolkit.Aspire.EventStore.EventStoreSettings.ConnectionString.set -> void CommunityToolkit.Aspire.EventStore.EventStoreSettings.DisableHealthChecks.get -> bool CommunityToolkit.Aspire.EventStore.EventStoreSettings.DisableHealthChecks.set -> void -CommunityToolkit.Aspire.EventStore.EventStoreSettings.EventStoreSettings() -> void +CommunityToolkit.Aspire.EventStore.EventStoreSettings.HealthCheckTimeout.get -> System.TimeSpan? +CommunityToolkit.Aspire.EventStore.EventStoreSettings.HealthCheckTimeout.set -> void CommunityToolkit.Aspire.EventStore.EventStoreSettings.DisableTracing.get -> bool CommunityToolkit.Aspire.EventStore.EventStoreSettings.DisableTracing.set -> void Microsoft.Extensions.Hosting.AspireEventStoreExtensions diff --git a/tests/CommunityToolkit.Aspire.EventStore.Tests/ConfigurationTests.cs b/tests/CommunityToolkit.Aspire.EventStore.Tests/ConfigurationTests.cs index e799bc18..e82a370e 100644 --- a/tests/CommunityToolkit.Aspire.EventStore.Tests/ConfigurationTests.cs +++ b/tests/CommunityToolkit.Aspire.EventStore.Tests/ConfigurationTests.cs @@ -13,6 +13,10 @@ public void ConnectionStringIsNullByDefault() => public void HealthChecksEnabledByDefault() => Assert.False(new EventStoreSettings().DisableHealthChecks); + [Fact] + public void HealthCheckTimeoutNullByDefault() => + Assert.Null(new EventStoreSettings().HealthCheckTimeout); + [Fact] public void DisableTracingIsFalseByDefault() => Assert.False(new EventStoreSettings().DisableTracing); diff --git a/tests/CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs b/tests/CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs index ec263722..b4e7b1e3 100644 --- a/tests/CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs +++ b/tests/CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs @@ -69,6 +69,7 @@ protected override (string json, string error)[] InvalidJsonToErrorMessage => ne protected override void SetHealthCheck(EventStoreSettings options, bool enabled) { options.DisableHealthChecks = !enabled; + options.HealthCheckTimeout = TimeSpan.FromMilliseconds(100); } protected override void SetMetrics(EventStoreSettings options, bool enabled) From ebd5908e98ad2fdc03b62878e29dfc67d61530f1 Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Mon, 25 Nov 2024 19:08:38 +1100 Subject: [PATCH 07/26] Remove HealthCheckTimeout from Conformance tests --- .../CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs b/tests/CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs index b4e7b1e3..ec263722 100644 --- a/tests/CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs +++ b/tests/CommunityToolkit.Aspire.EventStore.Tests/ConformanceTests.cs @@ -69,7 +69,6 @@ protected override (string json, string error)[] InvalidJsonToErrorMessage => ne protected override void SetHealthCheck(EventStoreSettings options, bool enabled) { options.DisableHealthChecks = !enabled; - options.HealthCheckTimeout = TimeSpan.FromMilliseconds(100); } protected override void SetMetrics(EventStoreSettings options, bool enabled) From 899886142f6e3e7c24b4f8cde4184211b6d73f73 Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Mon, 25 Nov 2024 20:01:35 +1100 Subject: [PATCH 08/26] Remove Atom Pub over HTTP as support is deprecated --- .../EventStoreBuilderExtensions.cs | 1 - .../AddEventStoreTests.cs | 5 ----- 2 files changed, 6 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs index a2ebfa7b..c4f9b87a 100644 --- a/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs @@ -204,6 +204,5 @@ private static void ConfigureEventStoreContainer(EnvironmentCallbackContext cont context.EnvironmentVariables.Add("EVENTSTORE_START_STANDARD_PROJECTIONS", "true"); context.EnvironmentVariables.Add("EVENTSTORE_NODE_PORT", $"{EventStoreResource.DefaultHttpPort}"); context.EnvironmentVariables.Add("EVENTSTORE_INSECURE", "true"); - context.EnvironmentVariables.Add("EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP", "true"); } } diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs index bcb8426d..1707a2fc 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/AddEventStoreTests.cs @@ -66,11 +66,6 @@ public async Task AddEventStoreContainerWithDefaultsAddsAnnotationMetadata() { Assert.Equal("EVENTSTORE_INSECURE", ext.Key); Assert.Equal("true", ext.Value); - }, - ext => - { - Assert.Equal("EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP", ext.Key); - Assert.Equal("true", ext.Value); }); } From 2daf3c8f7e19a21106080dc457a8c14c8021d19d Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Mon, 25 Nov 2024 21:12:09 +1100 Subject: [PATCH 09/26] Add Functional tests --- ...kit.Aspire.Hosting.EventStore.Tests.csproj | 13 +- .../EventStoreFunctionalTests.cs | 254 ++++++++++++++++++ 2 files changed, 256 insertions(+), 11 deletions(-) create mode 100644 tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests.csproj index b982e71b..60df07d6 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests.csproj +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests.csproj @@ -1,17 +1,8 @@ - + - - - - - - - - - - + diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs new file mode 100644 index 00000000..eea22c0c --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs @@ -0,0 +1,254 @@ +using Aspire.Components.Common.Tests; +using Aspire.Hosting; +using Aspire.Hosting.Utils; +using CommunityToolkit.Aspire.Testing; +using EventStore.Client; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using System.Text; +using System.Text.Json; +using Xunit.Abstractions; + +namespace CommunityToolkit.Aspire.Hosting.EventStore.Tests; + +[RequiresDocker] +public class EventStoreFunctionalTests(ITestOutputHelper testOutputHelper) +{ + public const string TestStreamNamePrefix = "account-"; + public const string TestAccountName = "John Doe"; + + [Fact] + public async Task VerifyEventStoreResource() + { + using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + + var eventstore = builder.AddEventStore("eventstore"); + + using var app = builder.Build(); + + await app.StartAsync(); + +#pragma warning disable CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + await app.WaitForTextAsync("Processor ConnectorsStreamSupervisor Running", "eventstore"); +#pragma warning restore CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + var hostBuilder = Host.CreateApplicationBuilder(); + + hostBuilder.Configuration[$"ConnectionStrings:{eventstore.Resource.Name}"] = await eventstore.Resource.ConnectionStringExpression.GetValueAsync(default); + + hostBuilder.AddEventStoreClient(eventstore.Resource.Name); + + using var host = hostBuilder.Build(); + + await host.StartAsync(); + + var eventStoreClient = host.Services.GetRequiredService(); + + await CreateTestData(eventStoreClient); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) + { + string? volumeName = null; + string? bindMountPath = null; + Guid? id = null; + + try + { + using var builder1 = TestDistributedApplicationBuilder.Create(testOutputHelper); + + var eventstore1 = builder1.AddEventStore("eventstore"); + + if (useVolume) + { + // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails +#pragma warning disable CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + volumeName = VolumeNameGenerator.CreateVolumeName(eventstore1, nameof(WithDataShouldPersistStateBetweenUsages)); +#pragma warning restore CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + // if the volume already exists (because of a crashing previous run), delete it + DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); + eventstore1.WithDataVolume(volumeName); + } + else + { + bindMountPath = Directory.CreateTempSubdirectory().FullName; + eventstore1.WithDataBindMount(bindMountPath); + } + + using (var app = builder1.Build()) + { + await app.StartAsync(); + +#pragma warning disable CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + await app.WaitForTextAsync("Processor ConnectorsStreamSupervisor Running", eventstore1.Resource.Name); +#pragma warning restore CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + try + { + var hostBuilder = Host.CreateApplicationBuilder(); + + hostBuilder.Configuration[$"ConnectionStrings:{eventstore1.Resource.Name}"] = await eventstore1.Resource.ConnectionStringExpression.GetValueAsync(default); + + hostBuilder.AddEventStoreClient(eventstore1.Resource.Name); + + using (var host = hostBuilder.Build()) + { + await host.StartAsync(); + + var eventStoreClient = host.Services.GetRequiredService(); + id = await CreateTestData(eventStoreClient); + } + } + finally + { + // Stops the container, or the Volume would still be in use + await app.StopAsync(); + } + } + + using var builder2 = TestDistributedApplicationBuilder.Create(testOutputHelper); + + var eventstore2 = builder2.AddEventStore("eventstore"); + + if (useVolume) + { + eventstore2.WithDataVolume(volumeName); + } + else + { + eventstore2.WithDataBindMount(bindMountPath!); + } + + using (var app = builder2.Build()) + { + await app.StartAsync(); + +#pragma warning disable CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + await app.WaitForTextAsync("Processor ConnectorsStreamSupervisor Running", eventstore2.Resource.Name); +#pragma warning restore CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + try + { + var hostBuilder = Host.CreateApplicationBuilder(); + + hostBuilder.Configuration[$"ConnectionStrings:{eventstore2.Resource.Name}"] = await eventstore2.Resource.ConnectionStringExpression.GetValueAsync(default); + + hostBuilder.AddEventStoreClient(eventstore2.Resource.Name); + + using (var host = hostBuilder.Build()) + { + await host.StartAsync(); + var eventStoreClient = host.Services.GetRequiredService(); + + await VerifyTestData(eventStoreClient, id.Value); + } + } + finally + { + // Stops the container, or the Volume would still be in use + await app.StopAsync(); + } + } + + } + finally + { + if (volumeName is not null) + { + DockerUtils.AttemptDeleteDockerVolume(volumeName); + } + + if (bindMountPath is not null) + { + try + { + Directory.Delete(bindMountPath, recursive: true); + } + catch + { + // Don't fail test if we can't clean the temporary folder + } + } + } + } + + [Fact] + public async Task VerifyWaitForEventStoreBlocksDependentResources() + { + var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); + using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + + var healthCheckTcs = new TaskCompletionSource(); + builder.Services.AddHealthChecks().AddAsyncCheck("blocking_check", () => + { + return healthCheckTcs.Task; + }); + + var resource = builder.AddEventStore("resource") + .WithHealthCheck("blocking_check"); + + var dependentResource = builder.AddEventStore("dependentresource") + .WaitFor(resource); + + using var app = builder.Build(); + + var pendingStart = app.StartAsync(cts.Token); + + var rns = app.Services.GetRequiredService(); + + await rns.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running, cts.Token); + + await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting, cts.Token); + + healthCheckTcs.SetResult(HealthCheckResult.Healthy()); + + await rns.WaitForResourceAsync(resource.Resource.Name, re => re.Snapshot.HealthStatus == HealthStatus.Healthy, cts.Token); + + await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, cts.Token); + + await pendingStart; + + await app.StopAsync(); + } + + private static async Task CreateTestData(EventStoreClient eventStoreClient) + { + var id = Guid.NewGuid(); + var accountCreated = new AccountCreated(id, TestAccountName); + var data = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(accountCreated)); + var eventData = new EventData(Uuid.NewUuid(), nameof(AccountCreated), data); + var streamName = $"{TestStreamNamePrefix}{id}"; + + var writeResult = await eventStoreClient.AppendToStreamAsync(streamName, StreamRevision.None, [eventData]); + Assert.NotNull(writeResult); + + await VerifyTestData(eventStoreClient, id); + + return id; + } + + private static async Task VerifyTestData(EventStoreClient eventStoreClient, Guid id) + { + var streamName = $"{TestStreamNamePrefix}{id}"; + + var readResult = eventStoreClient.ReadStreamAsync(Direction.Forwards, streamName, StreamPosition.Start); + Assert.NotNull(readResult); + + var readState = await readResult.ReadState; + Assert.Equal(ReadState.Ok, readState); + + await foreach (var resolvedEvent in readResult) + { + var @event = JsonSerializer.Deserialize(Encoding.UTF8.GetString(resolvedEvent.Event.Data.Span)); + Assert.NotNull(@event); + Assert.Equal(id, @event.Id); + Assert.Equal(TestAccountName, @event.Name); + } + } + + private sealed record AccountCreated(Guid Id, string Name); +} From 1d93d6c8ba38586cee0ec34657cda1ca96a31fb7 Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Tue, 26 Nov 2024 08:52:39 +1100 Subject: [PATCH 10/26] Log volume and mount can be added manually --- .../EventStoreBuilderExtensions.cs | 61 ------------ .../PublicAPI.Shipped.txt | 2 - .../EventStorePublicApiTests.cs | 95 ------------------- 3 files changed, 158 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs index c4f9b87a..64a446f3 100644 --- a/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs @@ -136,67 +136,6 @@ public static IResourceBuilder WithDataBindMount(this IResou return builder.WithBindMount(source, DataTargetFolder); } - /// - /// Adds a named volume for the log folder to a EventStore container resource. - /// - /// The resource builder. - /// The name of the volume. Defaults to an auto-generated name based on the application and resource names. - /// The . - /// - /// - /// Add an EventStore container to the application model and reference it in a .NET project. Additionally, in this - /// example a log volume is added to the container to allow logs to be persisted across container restarts. - /// - /// var builder = DistributedApplication.CreateBuilder(args); - /// - /// var eventstore = builder.AddEventStore("eventstore") - /// .WithLogVolume(); - /// var api = builder.AddProject<Projects.Api>("api") - /// .WithReference(eventstore); - /// - /// builder.Build().Run(); - /// - /// - /// - public static IResourceBuilder WithLogVolume(this IResourceBuilder builder, string? name = null) - { - ArgumentNullException.ThrowIfNull(builder); - -#pragma warning disable CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "log"), LogTargetFolder); -#pragma warning restore CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - } - - /// - /// Adds a bind mount for the log folder to a EventStore container resource. - /// - /// The resource builder. - /// The source directory on the host to mount into the container. - /// The . - /// - /// - /// Add an EventStore container to the application model and reference it in a .NET project. Additionally, in this - /// example a bind mount is added to the container to allow logs to be persisted across container restarts. - /// - /// var builder = DistributedApplication.CreateBuilder(args); - /// - /// var eventstore = builder.AddEventStore("eventstore") - /// .WithLogBindMount("./data/eventstore/logs"); - /// var api = builder.AddProject<Projects.Api>("api") - /// .WithReference(eventstore); - /// - /// builder.Build().Run(); - /// - /// - /// - public static IResourceBuilder WithLogBindMount(this IResourceBuilder builder, string source) - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(source); - - return builder.WithBindMount(source, LogTargetFolder); - } - private static void ConfigureEventStoreContainer(EnvironmentCallbackContext context) { context.EnvironmentVariables.Add("EVENTSTORE_CLUSTER_SIZE", "1"); diff --git a/src/CommunityToolkit.Aspire.Hosting.EventStore/PublicAPI.Shipped.txt b/src/CommunityToolkit.Aspire.Hosting.EventStore/PublicAPI.Shipped.txt index 8711a329..793ae86c 100644 --- a/src/CommunityToolkit.Aspire.Hosting.EventStore/PublicAPI.Shipped.txt +++ b/src/CommunityToolkit.Aspire.Hosting.EventStore/PublicAPI.Shipped.txt @@ -7,5 +7,3 @@ Aspire.Hosting.EventStoreBuilderExtensions static Aspire.Hosting.EventStoreBuilderExtensions.AddEventStore(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, int? port = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.EventStoreBuilderExtensions.WithDataVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.EventStoreBuilderExtensions.WithDataBindMount(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! -static Aspire.Hosting.EventStoreBuilderExtensions.WithLogVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! -static Aspire.Hosting.EventStoreBuilderExtensions.WithLogBindMount(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStorePublicApiTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStorePublicApiTests.cs index aa63951a..4546d29d 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStorePublicApiTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStorePublicApiTests.cs @@ -126,101 +126,6 @@ public void WithDataBindMountShouldAddMountAnnotation() Assert.Equal("/var/lib/eventstore", mountAnnotation.Target); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public void WithLogShouldThrowWhenBuilderIsNull(bool useVolume) - { - IResourceBuilder builder = null!; - - Func>? action = null; - - if (useVolume) - { - action = () => builder.WithLogVolume(); - } - else - { - const string source = "/data"; - - action = () => builder.WithLogBindMount(source); - } - - var exception = Assert.Throws(action); - Assert.Equal(nameof(builder), exception.ParamName); - } - - [Fact] - public void WithLogBindMountShouldThrowWhenSourceIsNull() - { - var builder = new DistributedApplicationBuilder([]); - var eventstore = builder.AddEventStore("eventstore"); - - string source = null!; - - var action = () => eventstore.WithLogBindMount(source); - - var exception = Assert.Throws(action); - Assert.Equal(nameof(source), exception.ParamName); - } - - [Fact] - public void WithLogVolumeShouldAddMountAnnotation() - { - var builder = new DistributedApplicationBuilder([]); - var eventstore = builder.AddEventStore("eventstore") - .WithLogVolume(name: null); - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var resource = appModel.Resources.OfType().SingleOrDefault(); - - Assert.NotNull(resource); - Assert.Equal("eventstore", resource.Name); - - Assert.True(resource.TryGetLastAnnotation(out ContainerMountAnnotation? mountAnnotation)); - Assert.EndsWith("-log", mountAnnotation.Source); - Assert.Equal("/var/log/eventstore", mountAnnotation.Target); - } - - [Fact] - public void WithNamedLogVolumeShouldAddMountAnnotation() - { - var builder = new DistributedApplicationBuilder([]); - var eventstore = builder.AddEventStore("eventstore") - .WithLogVolume("eventstore-logs"); - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var resource = appModel.Resources.OfType().SingleOrDefault(); - - Assert.NotNull(resource); - Assert.Equal("eventstore", resource.Name); - - Assert.True(resource.TryGetLastAnnotation(out ContainerMountAnnotation? mountAnnotation)); - Assert.Equal("eventstore-logs", mountAnnotation.Source); - Assert.Equal("/var/log/eventstore", mountAnnotation.Target); - } - - [Fact] - public void WithLogBindMountShouldAddMountAnnotation() - { - var builder = new DistributedApplicationBuilder([]); - var eventstore = builder.AddEventStore("eventstore") - .WithLogBindMount("./mylogs"); - using var app = builder.Build(); - - var appModel = app.Services.GetRequiredService(); - var resource = appModel.Resources.OfType().SingleOrDefault(); - - Assert.NotNull(resource); - Assert.Equal("eventstore", resource.Name); - - Assert.True(resource.TryGetLastAnnotation(out ContainerMountAnnotation? mountAnnotation)); - Assert.EndsWith("mylogs", mountAnnotation.Source); - Assert.Equal("/var/log/eventstore", mountAnnotation.Target); - } - [Fact] public void EventStoreResourceCtorShouldThrowWhenNameIsNull() { From d0c35a026d914dcebbec41bef576122bf758ce35 Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 28 Nov 2024 10:56:02 +1100 Subject: [PATCH 11/26] Remove unused const --- .../EventStoreBuilderExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs index 64a446f3..e2795933 100644 --- a/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.EventStore/EventStoreBuilderExtensions.cs @@ -16,7 +16,6 @@ namespace Aspire.Hosting; public static class EventStoreBuilderExtensions { private const string DataTargetFolder = "/var/lib/eventstore"; - private const string LogTargetFolder = "/var/log/eventstore"; /// /// Adds an EventStore resource to the application model. A container is used for local development. From 8553d6fb4489b2acdd24ae4524451c4c60ee4f24 Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 28 Nov 2024 11:00:01 +1100 Subject: [PATCH 12/26] Update EventStoreFunctionalTests.cs --- .../EventStoreFunctionalTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs index eea22c0c..b549a107 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs @@ -17,7 +17,7 @@ public class EventStoreFunctionalTests(ITestOutputHelper testOutputHelper) public const string TestStreamNamePrefix = "account-"; public const string TestAccountName = "John Doe"; - [Fact] + [Fact(Skip = "Finding root cause for test issue")] public async Task VerifyEventStoreResource() { using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); @@ -47,7 +47,7 @@ public async Task VerifyEventStoreResource() await CreateTestData(eventStoreClient); } - [Theory] + [Theory(Skip = "Finding root cause for test issue")] [InlineData(true)] [InlineData(false)] public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) @@ -176,7 +176,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) } } - [Fact] + [Fact(Skip = "Finding root cause for test issue")] public async Task VerifyWaitForEventStoreBlocksDependentResources() { var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); From 6a1b8594053e058e5b97273dda78bb68552871cf Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 28 Nov 2024 12:56:35 +1100 Subject: [PATCH 13/26] Update EventStoreFunctionalTests.cs --- .../EventStoreFunctionalTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs index b549a107..447cc8fd 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs @@ -17,7 +17,7 @@ public class EventStoreFunctionalTests(ITestOutputHelper testOutputHelper) public const string TestStreamNamePrefix = "account-"; public const string TestAccountName = "John Doe"; - [Fact(Skip = "Finding root cause for test issue")] + [Fact] public async Task VerifyEventStoreResource() { using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); From 9fc57ec3b8595aa5e9210feccede51597a2e7471 Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 28 Nov 2024 13:11:44 +1100 Subject: [PATCH 14/26] Update EventStoreFunctionalTests.cs --- .../EventStoreFunctionalTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs index 447cc8fd..7cb22e39 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs @@ -17,7 +17,7 @@ public class EventStoreFunctionalTests(ITestOutputHelper testOutputHelper) public const string TestStreamNamePrefix = "account-"; public const string TestAccountName = "John Doe"; - [Fact] + [Fact(Skip = "Finding root cause for test issue")] public async Task VerifyEventStoreResource() { using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); @@ -47,7 +47,7 @@ public async Task VerifyEventStoreResource() await CreateTestData(eventStoreClient); } - [Theory(Skip = "Finding root cause for test issue")] + [Theory] [InlineData(true)] [InlineData(false)] public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) From 90fceffb57de9296523e6fab45a72ba4c03da2bf Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 28 Nov 2024 13:25:05 +1100 Subject: [PATCH 15/26] Wait for resource to be healthy --- .../EventStoreFunctionalTests.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs index 7cb22e39..8e5d6456 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs @@ -28,9 +28,9 @@ public async Task VerifyEventStoreResource() await app.StartAsync(); -#pragma warning disable CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - await app.WaitForTextAsync("Processor ConnectorsStreamSupervisor Running", "eventstore"); -#pragma warning restore CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + var rns = app.Services.GetRequiredService(); + + await rns.WaitForResourceHealthyAsync(eventstore.Resource.Name, default); var hostBuilder = Host.CreateApplicationBuilder(); @@ -83,9 +83,9 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) { await app.StartAsync(); -#pragma warning disable CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - await app.WaitForTextAsync("Processor ConnectorsStreamSupervisor Running", eventstore1.Resource.Name); -#pragma warning restore CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + var rns = app.Services.GetRequiredService(); + + await rns.WaitForResourceHealthyAsync(eventstore1.Resource.Name, default); try { @@ -127,9 +127,9 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) { await app.StartAsync(); -#pragma warning disable CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - await app.WaitForTextAsync("Processor ConnectorsStreamSupervisor Running", eventstore2.Resource.Name); -#pragma warning restore CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + var rns = app.Services.GetRequiredService(); + + await rns.WaitForResourceHealthyAsync(eventstore1.Resource.Name, default); try { From 58f537cff197881fc7355a973c39dc849bd30773 Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 28 Nov 2024 13:25:36 +1100 Subject: [PATCH 16/26] Update EventStoreFunctionalTests.cs --- .../EventStoreFunctionalTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs index 8e5d6456..159b5faa 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs @@ -17,7 +17,7 @@ public class EventStoreFunctionalTests(ITestOutputHelper testOutputHelper) public const string TestStreamNamePrefix = "account-"; public const string TestAccountName = "John Doe"; - [Fact(Skip = "Finding root cause for test issue")] + [Fact] public async Task VerifyEventStoreResource() { using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); @@ -47,7 +47,7 @@ public async Task VerifyEventStoreResource() await CreateTestData(eventStoreClient); } - [Theory] + [Theory(Skip = "Finding root cause for test issue")] [InlineData(true)] [InlineData(false)] public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) From 61d26763c2dbefba320e075f24225a58e6de9e2d Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 28 Nov 2024 13:43:01 +1100 Subject: [PATCH 17/26] Update EventStoreFunctionalTests.cs --- .../EventStoreFunctionalTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs index 159b5faa..68131f57 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs @@ -44,10 +44,10 @@ public async Task VerifyEventStoreResource() var eventStoreClient = host.Services.GetRequiredService(); - await CreateTestData(eventStoreClient); + await CreateTestData(eventStoreClient);s } - [Theory(Skip = "Finding root cause for test issue")] + [Theory] [InlineData(true)] [InlineData(false)] public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) From 396fe02e0d8e9e54c7938dda5d1764d4fa21510a Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 28 Nov 2024 13:44:07 +1100 Subject: [PATCH 18/26] Fix typo --- .../EventStoreFunctionalTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs index 68131f57..f21796cf 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs @@ -44,7 +44,7 @@ public async Task VerifyEventStoreResource() var eventStoreClient = host.Services.GetRequiredService(); - await CreateTestData(eventStoreClient);s + await CreateTestData(eventStoreClient); } [Theory] From 20e91bf2b7f8882014369572089ffc3c6e66469e Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 28 Nov 2024 14:11:21 +1100 Subject: [PATCH 19/26] Use Cts --- .../EventStoreFunctionalTests.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs index f21796cf..d5c51ace 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs @@ -79,13 +79,14 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) eventstore1.WithDataBindMount(bindMountPath); } + var cts1 = new CancellationTokenSource(TimeSpan.FromMinutes(2)); using (var app = builder1.Build()) { - await app.StartAsync(); + await app.StartAsync(cts1.Token); var rns = app.Services.GetRequiredService(); - await rns.WaitForResourceHealthyAsync(eventstore1.Resource.Name, default); + await rns.WaitForResourceHealthyAsync(eventstore1.Resource.Name, cts1.Token); try { @@ -97,7 +98,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) using (var host = hostBuilder.Build()) { - await host.StartAsync(); + await host.StartAsync(cts1.Token); var eventStoreClient = host.Services.GetRequiredService(); id = await CreateTestData(eventStoreClient); @@ -123,13 +124,14 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) eventstore2.WithDataBindMount(bindMountPath!); } + var cts2 = new CancellationTokenSource(TimeSpan.FromMinutes(2)); using (var app = builder2.Build()) { - await app.StartAsync(); + await app.StartAsync(cts2.Token); var rns = app.Services.GetRequiredService(); - await rns.WaitForResourceHealthyAsync(eventstore1.Resource.Name, default); + await rns.WaitForResourceHealthyAsync(eventstore1.Resource.Name, cts2.Token); try { @@ -141,7 +143,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) using (var host = hostBuilder.Build()) { - await host.StartAsync(); + await host.StartAsync(cts2.Token); var eventStoreClient = host.Services.GetRequiredService(); await VerifyTestData(eventStoreClient, id.Value); From cdae698d1731a9168c3e563ae96a36e574c93263 Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 28 Nov 2024 14:25:33 +1100 Subject: [PATCH 20/26] Update EventStoreFunctionalTests.cs --- .../EventStoreFunctionalTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs index d5c51ace..0623dc6c 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs @@ -79,7 +79,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) eventstore1.WithDataBindMount(bindMountPath); } - var cts1 = new CancellationTokenSource(TimeSpan.FromMinutes(2)); + var cts1 = new CancellationTokenSource(TimeSpan.FromMinutes(5)); using (var app = builder1.Build()) { await app.StartAsync(cts1.Token); @@ -124,7 +124,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) eventstore2.WithDataBindMount(bindMountPath!); } - var cts2 = new CancellationTokenSource(TimeSpan.FromMinutes(2)); + var cts2 = new CancellationTokenSource(TimeSpan.FromMinutes(5)); using (var app = builder2.Build()) { await app.StartAsync(cts2.Token); From 282d12210d0633a7f201eafe7f8c37fd201f3b9b Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 28 Nov 2024 14:47:40 +1100 Subject: [PATCH 21/26] Check last test --- .../EventStoreFunctionalTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs index 0623dc6c..0b52aa56 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs @@ -47,7 +47,7 @@ public async Task VerifyEventStoreResource() await CreateTestData(eventStoreClient); } - [Theory] + [Theory(Skip = "Finding root cause for test issue")] [InlineData(true)] [InlineData(false)] public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) @@ -178,7 +178,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) } } - [Fact(Skip = "Finding root cause for test issue")] + [Fact] public async Task VerifyWaitForEventStoreBlocksDependentResources() { var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); From 8011a92bcd388921628fdf7b9a04c351ee2b2fbf Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 28 Nov 2024 16:26:50 +1100 Subject: [PATCH 22/26] Update EventStoreFunctionalTests.cs --- .../EventStoreFunctionalTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs index 0b52aa56..fe85d3a8 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs @@ -181,7 +181,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) [Fact] public async Task VerifyWaitForEventStoreBlocksDependentResources() { - var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); + var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); var healthCheckTcs = new TaskCompletionSource(); @@ -193,7 +193,7 @@ public async Task VerifyWaitForEventStoreBlocksDependentResources() var resource = builder.AddEventStore("resource") .WithHealthCheck("blocking_check"); - var dependentResource = builder.AddEventStore("dependentresource") + var dependentResource = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22") .WaitFor(resource); using var app = builder.Build(); @@ -208,7 +208,7 @@ public async Task VerifyWaitForEventStoreBlocksDependentResources() healthCheckTcs.SetResult(HealthCheckResult.Healthy()); - await rns.WaitForResourceAsync(resource.Resource.Name, re => re.Snapshot.HealthStatus == HealthStatus.Healthy, cts.Token); + await rns.WaitForResourceHealthyAsync(resource.Resource.Name, cts.Token); await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, cts.Token); From 30f10e93944ef98e1c5938b22ca41a09a883f6c6 Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 28 Nov 2024 16:59:57 +1100 Subject: [PATCH 23/26] Trying all tests --- .../EventStoreFunctionalTests.cs | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs index fe85d3a8..24194363 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs @@ -1,7 +1,6 @@ using Aspire.Components.Common.Tests; using Aspire.Hosting; using Aspire.Hosting.Utils; -using CommunityToolkit.Aspire.Testing; using EventStore.Client; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; @@ -20,7 +19,7 @@ public class EventStoreFunctionalTests(ITestOutputHelper testOutputHelper) [Fact] public async Task VerifyEventStoreResource() { - using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper); var eventstore = builder.AddEventStore("eventstore"); @@ -44,22 +43,23 @@ public async Task VerifyEventStoreResource() var eventStoreClient = host.Services.GetRequiredService(); - await CreateTestData(eventStoreClient); + var id = await CreateTestDataAsync(eventStoreClient); + await VerifyTestDataAsync(eventStoreClient, id); } - [Theory(Skip = "Finding root cause for test issue")] + [Theory] [InlineData(true)] [InlineData(false)] public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) { + var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); string? volumeName = null; string? bindMountPath = null; Guid? id = null; try { - using var builder1 = TestDistributedApplicationBuilder.Create(testOutputHelper); - + using var builder1 = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper); var eventstore1 = builder1.AddEventStore("eventstore"); if (useVolume) @@ -79,14 +79,13 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) eventstore1.WithDataBindMount(bindMountPath); } - var cts1 = new CancellationTokenSource(TimeSpan.FromMinutes(5)); using (var app = builder1.Build()) { - await app.StartAsync(cts1.Token); + await app.StartAsync(); var rns = app.Services.GetRequiredService(); - await rns.WaitForResourceHealthyAsync(eventstore1.Resource.Name, cts1.Token); + await rns.WaitForResourceHealthyAsync(eventstore1.Resource.Name, cts.Token); try { @@ -98,10 +97,11 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) using (var host = hostBuilder.Build()) { - await host.StartAsync(cts1.Token); + await host.StartAsync(); var eventStoreClient = host.Services.GetRequiredService(); - id = await CreateTestData(eventStoreClient); + id = await CreateTestDataAsync(eventStoreClient); + await VerifyTestDataAsync(eventStoreClient, id.Value); } } finally @@ -111,8 +111,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) } } - using var builder2 = TestDistributedApplicationBuilder.Create(testOutputHelper); - + using var builder2 = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper); var eventstore2 = builder2.AddEventStore("eventstore"); if (useVolume) @@ -121,17 +120,18 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) } else { + //EventStore shutdown can be slightly delayed, so second instance might fail to start when using the same bind mount before shutdown. + await Task.Delay(TimeSpan.FromSeconds(5)); eventstore2.WithDataBindMount(bindMountPath!); } - var cts2 = new CancellationTokenSource(TimeSpan.FromMinutes(5)); using (var app = builder2.Build()) { - await app.StartAsync(cts2.Token); + await app.StartAsync(); var rns = app.Services.GetRequiredService(); - await rns.WaitForResourceHealthyAsync(eventstore1.Resource.Name, cts2.Token); + await rns.WaitForResourceHealthyAsync(eventstore1.Resource.Name, cts.Token); try { @@ -143,10 +143,10 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) using (var host = hostBuilder.Build()) { - await host.StartAsync(cts2.Token); + await host.StartAsync(); var eventStoreClient = host.Services.GetRequiredService(); - await VerifyTestData(eventStoreClient, id.Value); + await VerifyTestDataAsync(eventStoreClient, id.Value); } } finally @@ -182,7 +182,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) public async Task VerifyWaitForEventStoreBlocksDependentResources() { var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); - using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper); var healthCheckTcs = new TaskCompletionSource(); builder.Services.AddHealthChecks().AddAsyncCheck("blocking_check", () => @@ -217,7 +217,7 @@ public async Task VerifyWaitForEventStoreBlocksDependentResources() await app.StopAsync(); } - private static async Task CreateTestData(EventStoreClient eventStoreClient) + private static async Task CreateTestDataAsync(EventStoreClient eventStoreClient) { var id = Guid.NewGuid(); var accountCreated = new AccountCreated(id, TestAccountName); @@ -228,12 +228,10 @@ private static async Task CreateTestData(EventStoreClient eventStoreClient var writeResult = await eventStoreClient.AppendToStreamAsync(streamName, StreamRevision.None, [eventData]); Assert.NotNull(writeResult); - await VerifyTestData(eventStoreClient, id); - return id; } - private static async Task VerifyTestData(EventStoreClient eventStoreClient, Guid id) + private static async Task VerifyTestDataAsync(EventStoreClient eventStoreClient, Guid id) { var streamName = $"{TestStreamNamePrefix}{id}"; From 0ff0242539afa08696eeed8ce8351b7ca4533c6c Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 28 Nov 2024 17:25:47 +1100 Subject: [PATCH 24/26] Update EventStoreFunctionalTests.cs --- .../EventStoreFunctionalTests.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs index 24194363..8de636be 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs @@ -52,7 +52,6 @@ public async Task VerifyEventStoreResource() [InlineData(false)] public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) { - var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); string? volumeName = null; string? bindMountPath = null; Guid? id = null; @@ -85,7 +84,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) var rns = app.Services.GetRequiredService(); - await rns.WaitForResourceHealthyAsync(eventstore1.Resource.Name, cts.Token); + await rns.WaitForResourceHealthyAsync(eventstore1.Resource.Name, default); try { @@ -131,7 +130,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) var rns = app.Services.GetRequiredService(); - await rns.WaitForResourceHealthyAsync(eventstore1.Resource.Name, cts.Token); + await rns.WaitForResourceHealthyAsync(eventstore1.Resource.Name, default); try { From d9c770b31f0ec8d8bdc37001871486e60f482264 Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 28 Nov 2024 17:44:49 +1100 Subject: [PATCH 25/26] Update EventStoreFunctionalTests.cs --- .../EventStoreFunctionalTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs index 8de636be..855e3d33 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs @@ -74,7 +74,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) } else { - bindMountPath = Directory.CreateTempSubdirectory().FullName; + bindMountPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); eventstore1.WithDataBindMount(bindMountPath); } From f0d33f8a6818490d9d3313cdaa3dbb40a97d1e04 Mon Sep 17 00:00:00 2001 From: Fredi Machado Date: Thu, 28 Nov 2024 18:24:34 +1100 Subject: [PATCH 26/26] Update EventStoreFunctionalTests.cs --- .../EventStoreFunctionalTests.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs index 855e3d33..f20a2833 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/EventStoreFunctionalTests.cs @@ -74,7 +74,19 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) } else { - bindMountPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + bindMountPath = Directory.CreateTempSubdirectory().FullName; + + if (!OperatingSystem.IsWindows()) + { + // Change permissions for non-root accounts (container user account) + const UnixFileMode OwnershipPermissions = + UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | + UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | + UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute; + + File.SetUnixFileMode(bindMountPath, OwnershipPermissions); + } + eventstore1.WithDataBindMount(bindMountPath); }