├── .config └── dotnet-tools.json ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── release.yml └── workflows │ ├── base.yml │ ├── ci.yml │ ├── dispatch-ce.yml │ ├── dispatch-ee.yml │ └── publish.yml ├── .gitignore ├── CHANGELOG.md ├── Directory.Build.props ├── Directory.Build.targets ├── KurrentDB.Client.sln ├── KurrentDB.Client.sln.DotSettings ├── LICENSE ├── README.md ├── gencert.ps1 ├── gencert.sh ├── ouro.png ├── ouro.svg ├── samples ├── Directory.Build.props ├── Samples.sln ├── Samples.sln.DotSettings ├── appending-events │ ├── Program.cs │ └── appending-events.csproj ├── connecting-to-a-cluster │ ├── Program.cs │ └── connecting-to-a-cluster.csproj ├── connecting-to-a-single-node │ ├── DemoInterceptor.cs │ ├── Program.cs │ └── connecting-to-a-single-node.csproj ├── diagnostics │ ├── Program.cs │ └── diagnostics.csproj ├── persistent-subscriptions │ ├── Program.cs │ └── persistent-subscriptions.csproj ├── projection-management │ ├── Program.cs │ └── projection-management.csproj ├── quick-start │ ├── Program.cs │ ├── docker-compose.yml │ └── quick-start.csproj ├── reading-events │ ├── Program.cs │ └── reading-events.csproj ├── secure-with-tls │ ├── .dockerignore │ ├── Dockerfile │ ├── Program.cs │ ├── README.md │ ├── create-certs.ps1 │ ├── create-certs.sh │ ├── docker-compose.app.yml │ ├── docker-compose.certs.yml │ ├── docker-compose.yml │ ├── run-app.sh │ └── secure-with-tls.csproj ├── server-side-filtering │ ├── Program.cs │ └── server-side-filtering.csproj ├── setting-up-dependency-injection │ ├── Controllers │ │ └── EventStoreController.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ └── setting-up-dependency-injection.csproj ├── subscribing-to-streams │ ├── Program.cs │ └── subscribing-to-streams.csproj └── user-certificates │ ├── Program.cs │ └── user-certificates.csproj ├── src ├── Directory.Build.props └── KurrentDB.Client │ ├── Core │ ├── Certificates │ │ └── X509Certificates.cs │ ├── ChannelBaseExtensions.cs │ ├── ChannelCache.cs │ ├── ChannelFactory.cs │ ├── ChannelInfo.cs │ ├── ChannelSelector.cs │ ├── ClusterMessage.cs │ ├── Common │ │ ├── AsyncStreamReaderExtensions.cs │ │ ├── Constants.cs │ │ ├── Diagnostics │ │ │ ├── ActivitySourceExtensions.cs │ │ │ ├── ActivityTagsCollectionExtensions.cs │ │ │ ├── Core │ │ │ │ ├── ActivityExtensions.cs │ │ │ │ ├── ActivityStatus.cs │ │ │ │ ├── ActivityStatusCodeHelper.cs │ │ │ │ ├── ActivityTagsCollectionExtensions.cs │ │ │ │ ├── ExceptionExtensions.cs │ │ │ │ ├── Telemetry │ │ │ │ │ └── TelemetryTags.cs │ │ │ │ └── Tracing │ │ │ │ │ ├── TracingConstants.cs │ │ │ │ │ └── TracingMetadata.cs │ │ │ ├── EventMetadataExtensions.cs │ │ │ ├── KurrentDBClientDiagnostics.cs │ │ │ ├── Telemetry │ │ │ │ └── TelemetryTags.cs │ │ │ └── Tracing │ │ │ │ └── TracingConstants.cs │ │ ├── EnumerableTaskExtensions.cs │ │ ├── EpochExtensions.cs │ │ ├── KurrentDBCallOptions.cs │ │ ├── MetadataExtensions.cs │ │ └── Shims │ │ │ ├── Index.cs │ │ │ ├── IsExternalInit.cs │ │ │ ├── Range.cs │ │ │ └── TaskCompletionSource.cs │ ├── DefaultRequestVersionHandler.cs │ ├── EndPointExtensions.cs │ ├── EventData.cs │ ├── EventRecord.cs │ ├── EventTypeFilter.cs │ ├── Exceptions │ │ ├── AccessDeniedException.cs │ │ ├── ConnectionString │ │ │ ├── ConnectionStringParseException.cs │ │ │ ├── DuplicateKeyException.cs │ │ │ ├── InvalidClientCertificateException.cs │ │ │ ├── InvalidHostException.cs │ │ │ ├── InvalidKeyValuePairException.cs │ │ │ ├── InvalidSchemeException.cs │ │ │ ├── InvalidSettingException.cs │ │ │ ├── InvalidUserCredentialsException.cs │ │ │ └── NoSchemeException.cs │ │ ├── DiscoveryException.cs │ │ ├── NotAuthenticatedException.cs │ │ ├── NotLeaderException.cs │ │ ├── RequiredMetadataPropertyMissingException.cs │ │ ├── ScavengeNotFoundException.cs │ │ ├── StreamDeletedException.cs │ │ ├── StreamNotFoundException.cs │ │ ├── UserNotFoundException.cs │ │ └── WrongExpectedVersionException.cs │ ├── FromAll.cs │ ├── FromStream.cs │ ├── GossipChannelSelector.cs │ ├── GrpcGossipClient.cs │ ├── GrpcServerCapabilitiesClient.cs │ ├── HashCode.cs │ ├── HttpFallback.cs │ ├── IChannelSelector.cs │ ├── IEventFilter.cs │ ├── IGossipClient.cs │ ├── IPosition.cs │ ├── IServerCapabilitiesClient.cs │ ├── Interceptors │ │ ├── ConnectionNameInterceptor.cs │ │ ├── ReportLeaderInterceptor.cs │ │ └── TypedExceptionInterceptor.cs │ ├── KurrentDBClientBase.cs │ ├── KurrentDBClientConnectivitySettings.cs │ ├── KurrentDBClientOperationOptions.cs │ ├── KurrentDBClientSerializationSettings.cs │ ├── KurrentDBClientSettings.ConnectionString.cs │ ├── KurrentDBClientSettings.cs │ ├── MessageData.cs │ ├── NodePreference.cs │ ├── NodePreferenceComparers.cs │ ├── NodeSelector.cs │ ├── OperationOptions.cs │ ├── Position.cs │ ├── PrefixFilterExpression.cs │ ├── ReconnectionRequired.cs │ ├── RegularFilterExpression.cs │ ├── ResolvedEvent.cs │ ├── Serialization │ │ ├── ISerializer.cs │ │ ├── Message.cs │ │ ├── MessageSerializer.cs │ │ ├── MessageTypeRegistry.cs │ │ ├── MessageTypeResolutionStrategy.cs │ │ ├── SchemaRegistry.cs │ │ ├── SystemTextJsonSerializer.cs │ │ └── TypeProvider.cs │ ├── ServerCapabilities.cs │ ├── SharingProvider.cs │ ├── SingleNodeChannelSelector.cs │ ├── SingleNodeHttpHandler.cs │ ├── StreamFilter.cs │ ├── StreamIdentifier.cs │ ├── StreamPosition.cs │ ├── StreamState.cs │ ├── SubscriptionDroppedReason.cs │ ├── SystemRoles.cs │ ├── SystemStreams.cs │ ├── TaskExtensions.cs │ ├── UserCredentials.cs │ ├── Uuid.cs │ └── protos │ │ ├── code.proto │ │ ├── gossip.proto │ │ ├── operations.proto │ │ ├── persistentsubscriptions.proto │ │ ├── projectionmanagement.proto │ │ ├── serverfeatures.proto │ │ ├── shared.proto │ │ ├── status.proto │ │ ├── streams.proto │ │ └── usermanagement.proto │ ├── KurrentDB.Client.csproj │ ├── OpenTelemetry │ └── TracerProviderBuilderExtensions.cs │ ├── Operations │ ├── DatabaseScavengeResult.cs │ ├── KurrentDBOperationsClient.Admin.cs │ ├── KurrentDBOperationsClient.Scavenge.cs │ ├── KurrentDBOperationsClient.cs │ ├── KurrentDBOperationsClientServiceCollectionExtensions.cs │ └── ScavengeResult.cs │ ├── PersistentSubscriptions │ ├── KurrentDBPersistentSubscriptionsClient.Create.cs │ ├── KurrentDBPersistentSubscriptionsClient.Delete.cs │ ├── KurrentDBPersistentSubscriptionsClient.Info.cs │ ├── KurrentDBPersistentSubscriptionsClient.List.cs │ ├── KurrentDBPersistentSubscriptionsClient.Read.cs │ ├── KurrentDBPersistentSubscriptionsClient.ReplayParked.cs │ ├── KurrentDBPersistentSubscriptionsClient.RestartSubsystem.cs │ ├── KurrentDBPersistentSubscriptionsClient.Update.cs │ ├── KurrentDBPersistentSubscriptionsClient.cs │ ├── KurrentDBPersistentSubscriptionsClientCollectionExtensions.cs │ ├── MaximumSubscribersReachedException.cs │ ├── PersistentSubscription.cs │ ├── PersistentSubscriptionDroppedByServerException.cs │ ├── PersistentSubscriptionExtraStatistic.cs │ ├── PersistentSubscriptionInfo.cs │ ├── PersistentSubscriptionMessage.cs │ ├── PersistentSubscriptionNakEventAction.cs │ ├── PersistentSubscriptionNotFoundException.cs │ ├── PersistentSubscriptionSettings.cs │ └── SystemConsumerStrategies.cs │ ├── ProjectionManagement │ ├── KurrentDBProjectionManagementClient.Control.cs │ ├── KurrentDBProjectionManagementClient.Create.cs │ ├── KurrentDBProjectionManagementClient.State.cs │ ├── KurrentDBProjectionManagementClient.Statistics.cs │ ├── KurrentDBProjectionManagementClient.Update.cs │ ├── KurrentDBProjectionManagementClient.cs │ ├── KurrentDBProjectionManagementClientCollectionExtensions.cs │ └── ProjectionDetails.cs │ ├── Streams │ ├── ConditionalWriteResult.cs │ ├── ConditionalWriteStatus.cs │ ├── DeadLine.cs │ ├── DeleteResult.cs │ ├── Direction.cs │ ├── IWriteResult.cs │ ├── InvalidTransactionException.cs │ ├── KurrentDBClient.Append.cs │ ├── KurrentDBClient.Delete.cs │ ├── KurrentDBClient.Metadata.cs │ ├── KurrentDBClient.Read.cs │ ├── KurrentDBClient.Subscriptions.cs │ ├── KurrentDBClient.Tombstone.cs │ ├── KurrentDBClient.cs │ ├── KurrentDBClientExtensions.cs │ ├── KurrentDBClientServiceCollectionExtensions.cs │ ├── MaximumAppendSizeExceededException.cs │ ├── ReadState.cs │ ├── StreamAcl.cs │ ├── StreamAclJsonConverter.cs │ ├── StreamMessage.cs │ ├── StreamMetadata.cs │ ├── StreamMetadataJsonConverter.cs │ ├── StreamMetadataResult.cs │ ├── StreamSubscription.cs │ ├── Streams │ │ ├── AppendReq.cs │ │ ├── BatchAppendReq.cs │ │ ├── BatchAppendResp.cs │ │ ├── DeleteReq.cs │ │ ├── ReadReq.cs │ │ └── TombstoneReq.cs │ ├── SubscriptionFilterOptions.cs │ ├── SuccessResult.cs │ ├── SystemEventTypes.cs │ ├── SystemMetadata.cs │ ├── SystemSettings.cs │ ├── SystemSettingsJsonConverter.cs │ ├── WriteResultExtensions.cs │ └── WrongExpectedVersionResult.cs │ └── UserManagement │ ├── KurrentDBUserManagementClient.cs │ ├── KurrentDBUserManagementClientCollectionExtensions.cs │ ├── KurrentDBUserManagerClientExtensions.cs │ └── UserDetails.cs └── test ├── Directory.Build.props ├── KurrentDB.Client.Tests.Common ├── .env ├── ApplicationInfo.cs ├── AssertEx.cs ├── Certificates.cs ├── Extensions │ ├── ConfigurationExtensions.cs │ ├── KurrentDBClientExtensions.cs │ ├── KurrentDBClientWarmupExtensions.cs │ ├── OperatingSystemExtensions.cs │ ├── ReadOnlyMemoryExtensions.cs │ ├── ShouldThrowAsyncExtensions.cs │ ├── TaskExtensions.cs │ ├── TypeExtensions.cs │ └── WithExtension.cs ├── Facts │ ├── AnonymousAccess.cs │ ├── Deprecation.cs │ └── Regression.cs ├── Fakers │ └── TestUserFaker.cs ├── Fixtures │ ├── BaseTestNode.cs │ ├── CertificatesManager.cs │ ├── KurrentDBFixtureOptions.cs │ ├── KurrentDBPermanentFixture.Helpers.cs │ ├── KurrentDBPermanentFixture.cs │ ├── KurrentDBPermanentTestNode.cs │ ├── KurrentDBTemporaryFixture.Helpers.cs │ ├── KurrentDBTemporaryFixture.cs │ └── KurrentDBTemporaryTestNode.cs ├── FluentDocker │ ├── FluentDockerBuilderExtensions.cs │ ├── FluentDockerServiceExtensions.cs │ ├── TestBypassService.cs │ ├── TestCompositeService.cs │ ├── TestContainerService.cs │ └── TestService.cs ├── GlobalEnvironment.cs ├── InterlockedBoolean.cs ├── KurrentDB.Client.Tests.Common.csproj ├── Logging.cs ├── PasswordGenerator.cs ├── Shouldly │ └── ShouldThrowAsyncExtensions.cs ├── TestCaseGenerator.cs ├── TestCredentials.cs ├── appsettings.Development.json ├── appsettings.json ├── docker-compose.certs.yml ├── docker-compose.cluster.yml ├── docker-compose.node.yml ├── docker-compose.yml └── shared.env ├── KurrentDB.Client.Tests.ExternalAssembly ├── ExternalEvents.cs └── KurrentDB.Client.Tests.ExternalAssembly.csproj ├── KurrentDB.Client.Tests.NeverLoadedAssembly ├── KurrentDB.Client.Tests.NeverLoadedAssembly.csproj └── NotLoadedExternalEvent.cs └── KurrentDB.Client.Tests ├── Assertions ├── ComparableAssertion.cs ├── EqualityAssertion.cs ├── NullArgumentAssertion.cs ├── StringConversionAssertion.cs └── ValueObjectAssertion.cs ├── AutoScenarioDataAttribute.cs ├── ClientCertificatesTests.cs ├── ConnectionStringTests.cs ├── Core └── Serialization │ ├── ContentTypeExtensionsTests.cs │ ├── MessageSerializerTests.cs │ ├── MessageTypeNamingResolutionContextTests.cs │ ├── MessageTypeRegistryTests.cs │ ├── NullMessageSerializerTests.cs │ ├── SchemaRegistryTests.cs │ └── TypeProviderTests.cs ├── FromAllTests.cs ├── FromStreamTests.cs ├── GossipChannelSelectorTests.cs ├── GrpcServerCapabilitiesClientTests.cs ├── InvalidCredentialsTestCases.cs ├── KurrentDB.Client.Tests.csproj ├── KurrentDBClientOperationsTests.cs ├── NodePreferenceComparerTests.cs ├── NodeSelectorTests.cs ├── Operations ├── AuthenticationTests.cs ├── MergeIndexTests.cs ├── ResignNodeTests.cs ├── RestartPersistentSubscriptionsTests.cs ├── ScavengeTests.cs ├── ShutdownNodeAuthenticationTests.cs └── ShutdownNodeTests.cs ├── PersistentSubscriptions ├── FilterTestCases.cs ├── SubscribeToAll │ ├── Obsolete │ │ ├── SubscribeToAllConnectToExistingWithStartFromNotSetObsoleteTests.cs │ │ ├── SubscribeToAllConnectToExistingWithStartFromSetToEndPositionObsoleteTests.cs │ │ ├── SubscribeToAllConnectWithoutReadPermissionsObsoleteTests.cs │ │ ├── SubscribeToAllFilterObsoleteTests.cs │ │ ├── SubscribeToAllGetInfoObsoleteTests.cs │ │ ├── SubscribeToAllListWithIncorrectCredentialsObsoleteTests.cs │ │ ├── SubscribeToAllNoDefaultCredentialsObsoleteTests.cs │ │ ├── SubscribeToAllObsoleteTests.cs │ │ ├── SubscribeToAllReplayParkedObsoleteTests.cs │ │ ├── SubscribeToAllResultWithNormalUserCredentialsObsoleteTests.cs │ │ ├── SubscribeToAllReturnsAllSubscriptionsObsoleteTests.cs │ │ ├── SubscribeToAllReturnsSubscriptionsToAllStreamObsoleteTests.cs │ │ ├── SubscribeToAllUpdateExistingWithCheckpointObsoleteTests.cs │ │ └── SubscribeToAllWithoutPSObsoleteTests.cs │ ├── SubscribeToAllConnectToExistingWithStartFromNotSetTests.cs │ ├── SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests.cs │ ├── SubscribeToAllConnectWithoutReadPermissionsTests.cs │ ├── SubscribeToAllFilterTests.cs │ ├── SubscribeToAllGetInfoTests.cs │ ├── SubscribeToAllListWithIncorrectCredentialsTests.cs │ ├── SubscribeToAllNoDefaultCredentialsTests.cs │ ├── SubscribeToAllReplayParkedTests.cs │ ├── SubscribeToAllResultWithNormalUserCredentialsTests.cs │ ├── SubscribeToAllReturnsAllSubscriptions.cs │ ├── SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs │ ├── SubscribeToAllTests.cs │ ├── SubscribeToAllUpdateExistingWithCheckpointTest.cs │ └── SubscribeToAllWithoutPSTests.cs └── SubscribeToStream │ ├── Obsolete │ ├── SubscribeToStreamConnectToExistingWithoutPermissionObsoleteTests.cs │ ├── SubscribeToStreamGetInfoObsoleteTests.cs │ └── SubscribeToStreamObsoleteTests.cs │ ├── SubscribeToStreamConnectToExistingWithStartFromBeginningTests.cs │ ├── SubscribeToStreamGetInfoTests.cs │ ├── SubscribeToStreamListTests.cs │ ├── SubscribeToStreamNoDefaultCredentialsTests.cs │ ├── SubscribeToStreamReplayParkedTests.cs │ └── SubscribeToStreamTests.cs ├── PositionTests.cs ├── ProjectionManagement ├── DisableProjectionTests.cs ├── EnableProjectionTests.cs ├── GetProjectionResultTests.cs ├── GetProjectionStateTests.cs ├── GetProjectionStatusTests.cs ├── ListAllProjectionsTests.cs ├── ListContinuousProjectionsTests.cs ├── ListOneTimeProjectionsTests.cs ├── ProjectionManagementTests.cs ├── ResetProjectionTests.cs ├── RestartSubsystemTests.cs └── UpdateProjectionTests.cs ├── RegularFilterExpressionTests.cs ├── Security ├── AllStreamWithNoAclSecurityTests.cs ├── DeleteStreamSecurityTests.cs ├── MultipleRoleSecurityTests.cs ├── OverridenSystemStreamSecurityForAllTests.cs ├── OverridenSystemStreamSecurityTests.cs ├── OverridenUserStreamSecurityTests.cs ├── ReadAllSecurityTests.cs ├── ReadStreamMetaSecurityTests.cs ├── ReadStreamSecurityTests.cs ├── SecurityFixture.cs ├── StreamSecurityInheritanceTests.cs ├── SubscribeToAllSecurityTests.cs ├── SubscribeToStreamSecurityTests.cs ├── SystemStreamSecurityTests.cs ├── WriteStreamMetaSecurityTests.cs └── WriteStreamSecurityTests.cs ├── SharingProviderTests.cs ├── StreamPositionTests.cs ├── StreamStateTests.cs ├── Streams ├── AppendTests.cs ├── Bugs │ └── Obsolete │ │ ├── Issue104.cs │ │ └── Issue2544.cs ├── DeleteTests.cs ├── Read │ ├── MessageBinaryData.cs │ ├── MessageDataComparer.cs │ ├── ReadAllEventsBackwardTests.cs │ ├── ReadAllEventsFixture.cs │ ├── ReadAllEventsForwardTests.cs │ ├── ReadStreamBackwardTests.cs │ ├── ReadStreamEventsLinkedToDeletedStreamTests.cs │ ├── ReadStreamForwardTests.cs │ └── ReadStreamWhenHavingMaxCountSetForStreamTests.cs ├── Serialization │ ├── SerializationTests.PersistentSubscriptions.cs │ ├── SerializationTests.Subscriptions.cs │ └── SerializationTests.cs ├── SoftDeleteTests.cs ├── StreamMetadataTests.cs ├── SubscriptionFilter.cs └── Subscriptions │ ├── Obsolete │ ├── SubscribeToAllObsoleteTests.cs │ └── SubscribeToStreamObsoleteTests.cs │ ├── SubscribeToAllTests.cs │ ├── SubscribeToStreamTests.cs │ └── SubscriptionDroppedResult.cs ├── UserManagement ├── ChangePasswordTests.cs ├── CreateUserTests.cs ├── DeleteUserTests.cs ├── DisableUserTests.cs ├── EnableUserTests.cs ├── GetCurrentUserTests.cs ├── ListUserTests.cs ├── ResettingUserPasswordTests.cs └── UserCredentialsTests.cs ├── UuidTests.cs ├── ValueObjectTests.cs └── X509CertificatesTests.cs /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "minver-cli": { 6 | "version": "4.2.0", 7 | "commands": [ 8 | "minver" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behaviour, in case users don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files we want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.c text 7 | *.h text 8 | *.cs text 9 | *.config text 10 | *.xml text 11 | *.manifest text 12 | *.bat text 13 | *.cmd text 14 | *.sh text 15 | *.txt text 16 | *.dat text 17 | *.rc text 18 | *.ps1 text 19 | *.csproj text 20 | *.sln text 21 | 22 | # Declare files that will always have CRLF line endings on checkout. 23 | 24 | # Denote all files that are truly binary and should not be modified. 25 | *.png binary 26 | *.jpg binary 27 | *.dll binary 28 | *.exe binary 29 | *.pdb binary 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. 16 | 2. 17 | 3. 18 | 4. 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Actual behavior** 24 | A clear and concise description of what actually happened. 25 | 26 | **Config/Logs/Screenshots** 27 | If applicable, please attach your node configuration, logs or any screenshots. 28 | 29 | **EventStore details** 30 | - EventStore server version: 31 | 32 | - Operating system: 33 | 34 | - EventStore client version (if applicable): 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | categories: 6 | - title: Added 7 | labels: 8 | - enhancement 9 | - title: Fixed 10 | labels: 11 | - bug 12 | - title: Changed 13 | labels: 14 | - "*" 15 | - title: Deprecated 16 | labels: 17 | - deprecated 18 | - title: Breaking Changes 19 | labels: 20 | - breaking 21 | - title: Documentation 22 | labels: 23 | - documentation -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | tags: 9 | - v* 10 | 11 | jobs: 12 | ce: 13 | uses: ./.github/workflows/base.yml 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | kurrentdb-tag: [ ci, lts ] 18 | test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement, Plugins, Security, Misc ] 19 | name: Test (${{ matrix.kurrentdb-tag }}) 20 | with: 21 | kurrentdb-tag: ${{ matrix.kurrentdb-tag }} 22 | test: ${{ matrix.test }} 23 | secrets: inherit 24 | 25 | ee: 26 | uses: ./.github/workflows/base.yml 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | kurrentdb-tag: [ previous-lts ] 31 | test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement, Security, Misc ] 32 | name: Test (${{ matrix.kurrentdb-tag }}) 33 | with: 34 | kurrentdb-tag: ${{ matrix.kurrentdb-tag }} 35 | test: ${{ matrix.test }} 36 | secrets: inherit 37 | -------------------------------------------------------------------------------- /.github/workflows/dispatch-ce.yml: -------------------------------------------------------------------------------- 1 | name: Dispatch CE 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | docker-tag: 7 | description: "Docker tag" 8 | required: true 9 | type: string 10 | docker-image: 11 | description: "Docker image" 12 | required: true 13 | type: string 14 | 15 | jobs: 16 | test: 17 | uses: ./.github/workflows/base.yml 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement, Security, Misc ] 22 | name: Test CE (${{ inputs.docker-tag }}) 23 | with: 24 | docker-tag: ${{ inputs.docker-tag }} 25 | docker-image: ${{ inputs.docker-image }} 26 | test: ${{ matrix.test }} 27 | secrets: 28 | CLOUDSMITH_CICD_USER: ${{ secrets.CLOUDSMITH_CICD_USER }} 29 | CLOUDSMITH_CICD_TOKEN: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/dispatch-ee.yml: -------------------------------------------------------------------------------- 1 | name: Dispatch 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | kurrentdb-tag: 7 | description: "The KurrentDB docker tag to use. If kurrentdb-image is empty, the action will use the values in the KURRENTDB_DOCKER_IMAGES variable (ci, lts, previous-lts)." 8 | required: true 9 | type: string 10 | kurrentdb-image: 11 | description: "The KurrentDB docker image to test against. Leave this empty to use the image in the KURRENTDB_DOCKER_IMAGES variable" 12 | required: false 13 | type: string 14 | kurrentdb-registry: 15 | description: "The docker registry containing the KurrentDB docker image. Leave this empty to use the registry in the KURRENTDB_DOCKER_IMAGES variable." 16 | required: false 17 | type: string 18 | jobs: 19 | test: 20 | uses: ./.github/workflows/base.yml 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement, Plugins ] 25 | name: Test (${{ inputs.kurrentdb-tag }}) 26 | with: 27 | kurrentdb-tag: ${{ inputs.kurrentdb-tag }} 28 | kurrentdb-image: ${{ inputs.kurrentdb-image }} 29 | kurrentdb-registry: ${{ inputs.kurrentdb-registry }} 30 | test: ${{ matrix.test }} 31 | secrets: inherit 32 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | net48;net8.0;net9.0 4 | true 5 | enable 6 | enable 7 | true 8 | preview 9 | 10 | Debug 11 | full 12 | pdbonly 13 | 14 | true 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KurrentDB .NET Client 2 | 3 | KurrentDB is the event-native database, where business events are immutably stored and streamed. Designed for event-sourced, event-driven, and microservices architectures 4 | 5 | This is the repository for the .NET client for KurrentDB version 20+ and uses gRPC as the communication protocol. 6 | 7 | ## Installation 8 | 9 | Reference the nuget package(s) for the API that you would like to call 10 | 11 | [KurrentDB.Client](https://www.nuget.org/packages/KurrentDB.Client) 12 | 13 | ## Support 14 | 15 | Information on support and commercial tools such as LDAP authentication can be found here: [Kurrent Support](https://kurrent.io/support/). 16 | 17 | ## CI Status 18 | 19 | ![Build](https://github.com/EventStore/EventStore-Client-Dotnet/actions/workflows/ci.yml/badge.svg) 20 | ![Build](https://github.com/EventStore/EventStore-Client-Dotnet/actions/workflows/lts.yml/badge.svg) 21 | ![Build](https://github.com/EventStore/EventStore-Client-Dotnet/actions/workflows/previous-lts.yml/badge.svg) 22 | 23 | ## Documentation 24 | 25 | Documentation for KurrentDB can be found here: [Kurrent Docs](https://kurrent.io/docs/). 26 | 27 | Bear in mind that this client is not yet properly documented. We are working hard on a new version of the documentation. 28 | 29 | ## Communities 30 | 31 | - [Discuss](https://discuss.kurrent.io/) 32 | - [Discord (Kurrent)](https://discord.gg/Phn9pmCw3t) 33 | - [Discord (ddd-cqrs-es)](https://discord.com/invite/sEZGSHNNbH) 34 | 35 | ## Contributing 36 | 37 | Development is done on the `master` branch. 38 | We attempt to do our best to ensure that the history remains clean and to do so, we generally ask contributors to squash their commits into a set or single logical commit. 39 | -------------------------------------------------------------------------------- /gencert.ps1: -------------------------------------------------------------------------------- 1 | Write-Host ">> Generating certificate..." 2 | 3 | # Create directory if it doesn't exist 4 | New-Item -ItemType Directory -Path .\certs -Force 5 | 6 | # Set permissions for the directory 7 | icacls .\certs /grant:r "$($env:UserName):(OI)(CI)F" 8 | 9 | # Pull the Docker image 10 | docker pull docker.kurrent.io/eventstore-utils/es-gencert-cli:latest 11 | 12 | docker run --rm --volume .\certs:/tmp docker.kurrent.io/eventstore-utils/es-gencert-cli create-ca -out /tmp/ca 13 | 14 | docker run --rm --volume .\certs:/tmp docker.kurrent.io/eventstore-utils/es-gencert-cli create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost 15 | 16 | # Create admin user 17 | docker run --rm --volume .\certs:/tmp docker.kurrent.io/eventstore-utils/es-gencert-cli create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin 18 | 19 | # Create an invalid user 20 | docker run --rm --volume .\certs:/tmp docker.kurrent.io/eventstore-utils/es-gencert-cli create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid 21 | 22 | # Set permissions recursively for the directory 23 | icacls .\certs /grant:r "$($env:UserName):(OI)(CI)F" 24 | -------------------------------------------------------------------------------- /ouro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurrent-io/KurrentDB-Client-Dotnet/1c39a1181c7fe82eba3712253041285ac98f20f9/ouro.png -------------------------------------------------------------------------------- /samples/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0 4 | enable 5 | enable 6 | true 7 | Exe 8 | preview 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/appending-events/appending-events.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | appending_events 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /samples/connecting-to-a-cluster/Program.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | 3 | #pragma warning disable CS8321 // Local function is declared but never used 4 | 5 | static void ConnectingToACluster() { 6 | #region connecting-to-a-cluster 7 | 8 | using var client = new KurrentDBClient( 9 | KurrentDBClientSettings.Create("esdb://localhost:1114,localhost:2114,localhost:3114") 10 | ); 11 | 12 | #endregion connecting-to-a-cluster 13 | } 14 | 15 | static void ProvidingDefaultCredentials() { 16 | #region providing-default-credentials 17 | 18 | using var client = new KurrentDBClient( 19 | KurrentDBClientSettings.Create("esdb://admin:changeit@localhost:1114,localhost:2114,localhost:3114") 20 | ); 21 | 22 | #endregion providing-default-credentials 23 | } 24 | 25 | static void ConnectingToAClusterComplex() { 26 | #region connecting-to-a-cluster-complex 27 | 28 | using var client = new KurrentDBClient( 29 | KurrentDBClientSettings.Create( 30 | "esdb://admin:changeit@localhost:1114,localhost:2114,localhost:3114?DiscoveryInterval=30000;GossipTimeout=10000;NodePreference=leader;MaxDiscoverAttempts=5" 31 | ) 32 | ); 33 | 34 | #endregion connecting-to-a-cluster-complex 35 | } 36 | -------------------------------------------------------------------------------- /samples/connecting-to-a-cluster/connecting-to-a-cluster.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | connecting_to_a_cluster 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /samples/connecting-to-a-single-node/DemoInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Grpc.Core; 2 | using Grpc.Core.Interceptors; 3 | 4 | namespace connecting_to_a_single_node; 5 | 6 | #region interceptor 7 | 8 | public class DemoInterceptor : Interceptor { 9 | public override AsyncServerStreamingCall 10 | AsyncServerStreamingCall( 11 | TRequest request, 12 | ClientInterceptorContext context, 13 | AsyncServerStreamingCallContinuation continuation 14 | ) { 15 | Console.WriteLine($"AsyncServerStreamingCall: {context.Method.FullName}"); 16 | 17 | return base.AsyncServerStreamingCall(request, context, continuation); 18 | } 19 | 20 | public override AsyncClientStreamingCall 21 | AsyncClientStreamingCall( 22 | ClientInterceptorContext context, 23 | AsyncClientStreamingCallContinuation continuation 24 | ) { 25 | Console.WriteLine($"AsyncClientStreamingCall: {context.Method.FullName}"); 26 | 27 | return base.AsyncClientStreamingCall(context, continuation); 28 | } 29 | } 30 | 31 | #endregion interceptor -------------------------------------------------------------------------------- /samples/connecting-to-a-single-node/connecting-to-a-single-node.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/diagnostics/diagnostics.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | connecting_to_a_cluster 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/persistent-subscriptions/persistent-subscriptions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | persistent_subscriptions 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /samples/projection-management/projection-management.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | projection_management 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/quick-start/Program.cs: -------------------------------------------------------------------------------- 1 | var tokenSource = new CancellationTokenSource(); 2 | var cancellationToken = tokenSource.Token; 3 | 4 | #region createClient 5 | 6 | const string connectionString = "esdb://admin:changeit@localhost:2113?tls=false&tlsVerifyCert=false"; 7 | 8 | var settings = KurrentDBClientSettings.Create(connectionString); 9 | 10 | var client = new KurrentDBClient(settings); 11 | 12 | #endregion createClient 13 | 14 | #region createEvent 15 | 16 | var evt = new TestEvent { 17 | EntityId = Guid.NewGuid().ToString("N"), 18 | ImportantData = "I wrote my first event!" 19 | }; 20 | 21 | #endregion createEvent 22 | 23 | #region appendEvents 24 | 25 | await client.AppendToStreamAsync( 26 | "some-stream", 27 | StreamState.Any, 28 | [evt], 29 | cancellationToken: cancellationToken 30 | ); 31 | 32 | #endregion appendEvents 33 | 34 | #region overriding-user-credentials 35 | 36 | await client.AppendToStreamAsync( 37 | "some-stream", 38 | StreamState.Any, 39 | [evt], 40 | new AppendToStreamOptions { UserCredentials = new UserCredentials("admin", "changeit") }, 41 | cancellationToken 42 | ); 43 | 44 | #endregion overriding-user-credentials 45 | 46 | #region readStream 47 | 48 | var result = client.ReadStreamAsync( 49 | "some-stream", 50 | cancellationToken: cancellationToken 51 | ); 52 | 53 | var events = await result 54 | .DeserializedData() 55 | .ToListAsync(cancellationToken); 56 | 57 | #endregion readStream 58 | 59 | public class TestEvent { 60 | public string? EntityId { get; set; } 61 | public string? ImportantData { get; set; } 62 | } 63 | -------------------------------------------------------------------------------- /samples/quick-start/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | eventstore: 4 | image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:latest 5 | environment: 6 | EVENTSTORE_INSECURE: true 7 | EVENTSTORE_MEM_DB: false 8 | EVENTSTORE_RUN_PROJECTIONS: all 9 | EVENTSTORE_START_STANDARD_PROJECTIONS: true 10 | EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP: true 11 | ports: 12 | - "2113:2113" -------------------------------------------------------------------------------- /samples/quick-start/quick-start.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | quick_start 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/reading-events/reading-events.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | reading_events 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/secure-with-tls/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.dockerignore 2 | **/.env 3 | **/.git 4 | **/.gitignore 5 | **/.project 6 | **/.settings 7 | **/.toolstarget 8 | **/.vs 9 | **/.vscode 10 | **/.idea 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /samples/secure-with-tls/create-certs.ps1: -------------------------------------------------------------------------------- 1 | New-Item -ItemType Directory -Force -Path certs 2 | 3 | docker-compose -f docker-compose.certs.yml up --remove-orphans 4 | 5 | Import-Certificate -FilePath ".\certs\ca\ca.crt" -CertStoreLocation Cert:\CurrentUser\Root 6 | -------------------------------------------------------------------------------- /samples/secure-with-tls/create-certs.sh: -------------------------------------------------------------------------------- 1 | unameOutput="$(uname -sr)" 2 | case "${unameOutput}" in 3 | Linux*Microsoft*) machine=WSL;; 4 | Linux*) machine=Linux;; 5 | Darwin*) machine=MacOS;; 6 | *) machine="${unameOutput}" 7 | esac 8 | 9 | echo ">> Generating certificate..." 10 | mkdir -p certs 11 | docker-compose -f docker-compose.certs.yml up --remove-orphans 12 | 13 | echo ">> Copying certificate..." 14 | cp certs/ca/ca.crt /usr/local/share/ca-certificates/eventstore_ca.crt 15 | #rsync --progress certs/ca/ca.crt /usr/local/share/ca-certificates/eventstore_ca.crt 16 | 17 | echo ">> Updating certificate permissions..." 18 | #chmod 644 /usr/local/share/ca-certificates/eventstore_ca.crt 19 | 20 | if [ "${machine}" == "MacOS" ]; then 21 | echo ">> Installing certificate on ${machine}..." 22 | sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /usr/local/share/ca-certificates/eventstore_ca.crt 23 | elif [ "$(machine)" == "Linux" ]; then 24 | echo ">> Installing certificate on ${machine}..." 25 | sudo update-ca-certificates 26 | elif [ "$(machine)" == "WSL" ]; then 27 | echo ">> Installing certificate on ${machine}..." 28 | sudo update-ca-certificates 29 | else 30 | echo ">> Unknown platform. Please install the certificate manually." 31 | fi -------------------------------------------------------------------------------- /samples/secure-with-tls/docker-compose.app.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | networks: 4 | default: 5 | name: eventstore-network 6 | 7 | services: 8 | 9 | app: 10 | container_name: app 11 | build: ./ 12 | environment: 13 | # URL should match the DNS name in certificate and container name 14 | - ESDB__CONNECTION__STRING=esdb://admin:changeit@eventstore:2113?Tls=true&tlsVerifyCert=false 15 | depends_on: 16 | eventstore: 17 | condition: service_healthy 18 | links: 19 | - eventstore 20 | -------------------------------------------------------------------------------- /samples/secure-with-tls/docker-compose.certs.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | networks: 4 | default: 5 | name: eventstore-network 6 | 7 | services: 8 | 9 | volumes-provisioner: 10 | image: hasnat/volumes-provisioner 11 | container_name: volumes-provisioner 12 | environment: 13 | PROVISION_DIRECTORIES: "1000:1000:0755:/tmp/certs" 14 | volumes: 15 | - "./certs:/tmp/certs" 16 | network_mode: none 17 | 18 | cert-gen: 19 | image: docker.eventstore.com/eventstore-utils/es-gencert-cli:latest 20 | container_name: cert-gen 21 | user: "1000:1000" 22 | entrypoint: [ "/bin/sh","-c" ] 23 | command: 24 | - | 25 | es-gencert-cli create-ca -out /tmp/certs/ca 26 | es-gencert-cli create-node -ca-certificate /tmp/certs/ca/ca.crt -ca-key /tmp/certs/ca/ca.key -out /tmp/certs/node -ip-addresses 127.0.0.1 -dns-names localhost,eventstore 27 | volumes: 28 | - "./certs:/tmp/certs" 29 | depends_on: 30 | - volumes-provisioner 31 | -------------------------------------------------------------------------------- /samples/secure-with-tls/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | networks: 4 | default: 5 | name: eventstore-network 6 | 7 | services: 8 | 9 | eventstore: 10 | image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:latest 11 | container_name: eventstore 12 | environment: 13 | - EVENTSTORE_MEM_DB=true 14 | - EVENTSTORE_HTTP_PORT=2113 15 | - EVENTSTORE_LOG_LEVEL=Information 16 | - EVENTSTORE_RUN_PROJECTIONS=None 17 | - EVENTSTORE_START_STANDARD_PROJECTIONS=true 18 | - EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=true 19 | 20 | # set certificates location 21 | - EVENTSTORE_CERTIFICATE_FILE=/etc/eventstore/certs/node/node.crt 22 | - EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE=/etc/eventstore/certs/node/node.key 23 | - EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH=/etc/eventstore/certs/ca 24 | ports: 25 | - "2113:2113" 26 | volumes: 27 | - ./certs:/etc/eventstore/certs 28 | - type: volume 29 | source: eventstore-volume-data1 30 | target: /var/lib/eventstore 31 | - type: volume 32 | source: eventstore-volume-logs1 33 | target: /var/log/eventstore 34 | restart: unless-stopped 35 | 36 | volumes: 37 | eventstore-volume-data1: 38 | eventstore-volume-logs1: 39 | -------------------------------------------------------------------------------- /samples/secure-with-tls/run-app.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Going down..." 4 | docker-compose -f docker-compose.yml -f docker-compose.app.yml down 5 | 6 | echo "Going up..." 7 | docker-compose -f docker-compose.yml -f docker-compose.app.yml up --remove-orphans 8 | -------------------------------------------------------------------------------- /samples/secure-with-tls/secure-with-tls.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net8.0 5 | secure_with_tls 6 | enable 7 | enable 8 | Linux 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /samples/server-side-filtering/server-side-filtering.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | server_side_filtering 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/setting-up-dependency-injection/Controllers/EventStoreController.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace setting_up_dependency_injection.Controllers { 5 | [ApiController] 6 | [Route("[controller]")] 7 | public class EventStoreController : ControllerBase { 8 | #region using-dependency 9 | private readonly KurrentDBClient _KurrentClient; 10 | 11 | public EventStoreController(KurrentDBClient KurrentDBClient) { 12 | _KurrentClient = KurrentDBClient; 13 | } 14 | 15 | [HttpGet] 16 | public IAsyncEnumerable Get() { 17 | return _KurrentClient.ReadAllAsync(); 18 | } 19 | #endregion using-dependency 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/setting-up-dependency-injection/Program.cs: -------------------------------------------------------------------------------- 1 | namespace setting_up_dependency_injection; 2 | 3 | public class Program { 4 | public static async Task Main(string[] args) { 5 | using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); 6 | await CreateHostBuilder(args).Build().WaitForShutdownAsync(cts.Token); 7 | } 8 | 9 | public static IHostBuilder CreateHostBuilder(string[] args) => 10 | Host.CreateDefaultBuilder(args) 11 | .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); 12 | } -------------------------------------------------------------------------------- /samples/setting-up-dependency-injection/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:58204", 8 | "sslPort": 44377 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "weatherforecast", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "setting_up_dependency_injection": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "weatherforecast", 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /samples/setting-up-dependency-injection/Startup.cs: -------------------------------------------------------------------------------- 1 | namespace setting_up_dependency_injection; 2 | 3 | public class Startup { 4 | public Startup(IConfiguration configuration) => Configuration = configuration; 5 | 6 | public IConfiguration Configuration { get; } 7 | 8 | public void ConfigureServices(IServiceCollection services) { 9 | services.AddControllers(); 10 | 11 | #region setting-up-dependency 12 | 13 | services.AddKurrentDBClient("esdb://admin:changeit@localhost:2113?tls=false"); 14 | 15 | #endregion setting-up-dependency 16 | } 17 | 18 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { 19 | if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); 20 | 21 | app.UseHttpsRedirection(); 22 | app.UseRouting(); 23 | app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/setting-up-dependency-injection/setting-up-dependency-injection.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | setting_up_dependency_injection 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /samples/subscribing-to-streams/subscribing-to-streams.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | subscribing_to_streams 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/user-certificates/Program.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | 3 | await ClientWithUserCertificates(); 4 | 5 | return; 6 | 7 | static async Task ClientWithUserCertificates() { 8 | try { 9 | # region client-with-user-certificates 10 | 11 | const string userCertFile = "/path/to/user.crt"; 12 | const string userKeyFile = "/path/to/user.key"; 13 | 14 | var settings = KurrentDBClientSettings.Create( 15 | $"esdb://localhost:2113/?tls=true&tlsVerifyCert=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}" 16 | ); 17 | 18 | await using var client = new KurrentDBClient(settings); 19 | 20 | # endregion client-with-user-certificates 21 | } catch (InvalidClientCertificateException) { 22 | // ignore for sample purposes 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /samples/user-certificates/user-certificates.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | user_certificates 7 | enable 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/ChannelBaseExtensions.cs: -------------------------------------------------------------------------------- 1 | using Grpc.Core; 2 | 3 | namespace KurrentDB.Client; 4 | 5 | static class ChannelBaseExtensions { 6 | public static async ValueTask DisposeAsync(this ChannelBase channel) { 7 | await channel.ShutdownAsync().ConfigureAwait(false); 8 | (channel as IDisposable)?.Dispose(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/ChannelInfo.cs: -------------------------------------------------------------------------------- 1 | using Grpc.Core; 2 | 3 | namespace KurrentDB.Client { 4 | #pragma warning disable 1591 5 | public record ChannelInfo( 6 | ChannelBase Channel, 7 | ServerCapabilities ServerCapabilities, 8 | CallInvoker CallInvoker); 9 | } 10 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/ChannelSelector.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Grpc.Core; 5 | 6 | namespace KurrentDB.Client { 7 | internal class ChannelSelector : IChannelSelector { 8 | private readonly IChannelSelector _inner; 9 | 10 | public ChannelSelector( 11 | KurrentDBClientSettings settings, 12 | ChannelCache channelCache) { 13 | _inner = settings.ConnectivitySettings.IsSingleNode 14 | ? new SingleNodeChannelSelector(settings, channelCache) 15 | : new GossipChannelSelector(settings, channelCache, new GrpcGossipClient(settings)); 16 | } 17 | 18 | public Task SelectChannelAsync(CancellationToken cancellationToken) => 19 | _inner.SelectChannelAsync(cancellationToken); 20 | 21 | public ChannelBase SelectChannel(DnsEndPoint endPoint) => 22 | _inner.SelectChannel(endPoint); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/ClusterMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace KurrentDB.Client { 4 | internal static class ClusterMessages { 5 | public record ClusterInfo(MemberInfo[] Members); 6 | 7 | public record MemberInfo(Uuid InstanceId, VNodeState State, bool IsAlive, DnsEndPoint EndPoint); 8 | 9 | public enum VNodeState { 10 | Initializing = 0, 11 | DiscoverLeader = 1, 12 | Unknown = 2, 13 | PreReplica = 3, 14 | CatchingUp = 4, 15 | Clone = 5, 16 | Follower = 6, 17 | PreLeader = 7, 18 | Leader = 8, 19 | Manager = 9, 20 | ShuttingDown = 10, 21 | Shutdown = 11, 22 | ReadOnlyLeaderless = 12, 23 | PreReadOnlyReplica = 13, 24 | ReadOnlyReplica = 14, 25 | ResigningLeader = 15 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/AsyncStreamReaderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Channels; 2 | using System.Runtime.CompilerServices; 3 | using Grpc.Core; 4 | 5 | namespace KurrentDB.Client; 6 | 7 | static class AsyncStreamReaderExtensions { 8 | public static async IAsyncEnumerable ReadAllAsync( 9 | this IAsyncStreamReader reader, 10 | [EnumeratorCancellation] 11 | CancellationToken cancellationToken = default 12 | ) { 13 | while (await reader.MoveNext(cancellationToken).ConfigureAwait(false)) 14 | yield return reader.Current; 15 | } 16 | 17 | public static async IAsyncEnumerable ReadAllAsync(this ChannelReader reader, [EnumeratorCancellation] CancellationToken cancellationToken = default) { 18 | #if NET 19 | await foreach (var item in reader.ReadAllAsync(cancellationToken)) 20 | yield return item; 21 | #else 22 | while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) { 23 | while (reader.TryRead(out T? item)) { 24 | yield return item; 25 | } 26 | } 27 | #endif 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/Diagnostics/ActivityTagsCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.CompilerServices; 3 | using KurrentDB.Client; 4 | using Kurrent.Diagnostics; 5 | using Kurrent.Diagnostics.Telemetry; 6 | 7 | namespace KurrentDB.Client.Diagnostics; 8 | 9 | static class ActivityTagsCollectionExtensions { 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | public static ActivityTagsCollection WithGrpcChannelServerTags(this ActivityTagsCollection tags, ChannelInfo? channelInfo) { 12 | if (channelInfo is null) 13 | return tags; 14 | 15 | var authorityParts = channelInfo.Channel.Target.Split(':'); 16 | 17 | return tags 18 | .WithRequiredTag(TelemetryTags.Server.Address, authorityParts[0]) 19 | .WithRequiredTag(TelemetryTags.Server.Port, int.Parse(authorityParts[1])); 20 | } 21 | 22 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 23 | public static ActivityTagsCollection WithClientSettingsServerTags(this ActivityTagsCollection source, KurrentDBClientSettings settings) { 24 | if (settings.ConnectivitySettings.DnsGossipSeeds?.Length != 1) 25 | return source; 26 | 27 | var gossipSeed = settings.ConnectivitySettings.DnsGossipSeeds[0]; 28 | 29 | return source 30 | .WithRequiredTag(TelemetryTags.Server.Address, gossipSeed.Host) 31 | .WithRequiredTag(TelemetryTags.Server.Port, gossipSeed.Port); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/Diagnostics/Core/ActivityStatus.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable CheckNamespace 2 | 3 | using System.Diagnostics; 4 | 5 | namespace Kurrent.Diagnostics; 6 | 7 | record ActivityStatus(ActivityStatusCode StatusCode, string? Description, Exception? Exception) { 8 | public static ActivityStatus Ok(string? description = null) => 9 | new(ActivityStatusCode.Ok, description, null); 10 | 11 | public static ActivityStatus Error(Exception exception, string? description = null) => 12 | new(ActivityStatusCode.Error, description ?? exception.Message, exception); 13 | } 14 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable CheckNamespace 2 | 3 | using System.Diagnostics; 4 | using System.Runtime.CompilerServices; 5 | 6 | using static System.Diagnostics.ActivityStatusCode; 7 | using static System.StringComparison; 8 | 9 | namespace Kurrent.Diagnostics; 10 | 11 | static class ActivityStatusCodeHelper { 12 | public const string UnsetStatusCodeTagValue = "UNSET"; 13 | public const string OkStatusCodeTagValue = "OK"; 14 | public const string ErrorStatusCodeTagValue = "ERROR"; 15 | 16 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 17 | public static string? GetTagValueForStatusCode(ActivityStatusCode statusCode) => 18 | statusCode switch { 19 | Unset => UnsetStatusCodeTagValue, 20 | Error => ErrorStatusCodeTagValue, 21 | Ok => OkStatusCodeTagValue, 22 | _ => null 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable CheckNamespace 2 | 3 | using System.Diagnostics; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace Kurrent.Diagnostics; 7 | 8 | static class ActivityTagsCollectionExtensions { 9 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 10 | public static ActivityTagsCollection WithRequiredTag(this ActivityTagsCollection source, string key, object? value) { 11 | source[key] = value ?? throw new ArgumentNullException(key); 12 | return source; 13 | } 14 | 15 | /// 16 | /// - If the key previously existed in the collection and the value is , the collection item matching the key will get removed from the collection. 17 | /// - If the key previously existed in the collection and the value is not , the value will replace the old value stored in the collection. 18 | /// - Otherwise, a new item will get added to the collection. 19 | /// 20 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 21 | public static ActivityTagsCollection WithOptionalTag(this ActivityTagsCollection source, string key, object? value) { 22 | source[key] = value; 23 | return source; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/Diagnostics/Core/ExceptionExtensions.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable CheckNamespace 2 | 3 | using System.Globalization; 4 | 5 | namespace Kurrent.Diagnostics; 6 | 7 | static class ExceptionExtensions { 8 | /// 9 | /// Returns a culture-independent string representation of the given object, 10 | /// appropriate for diagnostics tracing. 11 | /// 12 | /// Exception to convert to string. 13 | /// Exception as string with no culture. 14 | public static string ToInvariantString(this Exception exception) { 15 | var originalUiCulture = Thread.CurrentThread.CurrentUICulture; 16 | 17 | try { 18 | Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; 19 | return exception.ToString(); 20 | } 21 | finally { 22 | Thread.CurrentThread.CurrentUICulture = originalUiCulture; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable CheckNamespace 2 | 3 | namespace Kurrent.Diagnostics.Telemetry; 4 | 5 | // The attributes below match the specification of v1.24.0 of the Open Telemetry semantic conventions. 6 | // Some attributes are ignored where not required or relevant. 7 | // https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/general/trace.md 8 | // https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/database/database-spans.md 9 | // https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/exceptions/exceptions-spans.md 10 | 11 | static partial class TelemetryTags { 12 | public static class Database { 13 | public const string User = "db.user"; 14 | public const string System = "db.system"; 15 | public const string Operation = "db.operation"; 16 | } 17 | 18 | public static class Server { 19 | public const string Address = "server.address"; 20 | public const string Port = "server.port"; 21 | public const string SocketAddress = "server.socket.address"; // replaces: "net.peer.ip" (AttributeNetPeerIp) 22 | } 23 | 24 | public static class Exception { 25 | public const string EventName = "exception"; 26 | public const string Type = "exception.type"; 27 | public const string Message = "exception.message"; 28 | public const string Stacktrace = "exception.stacktrace"; 29 | } 30 | 31 | public static class Otel { 32 | public const string StatusCode = "otel.status_code"; 33 | public const string StatusDescription = "otel.status_description"; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/Diagnostics/Core/Tracing/TracingConstants.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable CheckNamespace 2 | 3 | namespace Kurrent.Diagnostics.Tracing; 4 | 5 | static partial class TracingConstants { 6 | public static class Metadata { 7 | public const string TraceId = "$traceId"; 8 | public const string SpanId = "$spanId"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/Diagnostics/Core/Tracing/TracingMetadata.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable CheckNamespace 2 | 3 | using System.Diagnostics; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace Kurrent.Diagnostics.Tracing; 7 | 8 | readonly record struct TracingMetadata( 9 | [property: JsonPropertyName(TracingConstants.Metadata.TraceId)] 10 | string? TraceId, 11 | [property: JsonPropertyName(TracingConstants.Metadata.SpanId)] 12 | string? SpanId 13 | ) { 14 | public static readonly TracingMetadata None = new(null, null); 15 | 16 | [JsonIgnore] public bool IsValid => TraceId != null && SpanId != null; 17 | 18 | public ActivityContext? ToActivityContext(bool isRemote = true) { 19 | try { 20 | return IsValid 21 | ? new ActivityContext( 22 | ActivityTraceId.CreateFromString(new ReadOnlySpan(TraceId!.ToCharArray())), 23 | ActivitySpanId.CreateFromString(new ReadOnlySpan(SpanId!.ToCharArray())), 24 | ActivityTraceFlags.Recorded, 25 | isRemote: isRemote 26 | ) 27 | : default; 28 | } catch (Exception) { 29 | return default; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/Diagnostics/KurrentDBClientDiagnostics.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace KurrentDB.Client.Diagnostics; 4 | 5 | public static class KurrentDBClientDiagnostics { 6 | public const string InstrumentationName = "kurrent"; 7 | public static readonly ActivitySource ActivitySource = new(InstrumentationName); 8 | } 9 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/Diagnostics/Telemetry/TelemetryTags.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable CheckNamespace 2 | 3 | namespace Kurrent.Diagnostics.Telemetry; 4 | 5 | static partial class TelemetryTags { 6 | public static class Kurrent { 7 | public const string Stream = "db.kurrent.stream"; 8 | public const string SubscriptionId = "db.kurrent.subscription.id"; 9 | public const string EventId = "db.kurrent.event.id"; 10 | public const string EventType = "db.kurrent.event.type"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable CheckNamespace 2 | 3 | namespace Kurrent.Diagnostics.Tracing; 4 | 5 | static partial class TracingConstants { 6 | public static class Operations { 7 | public const string Append = "streams.append"; 8 | public const string Subscribe = "streams.subscribe"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/EnumerableTaskExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace KurrentDB.Client; 4 | 5 | static class EnumerableTaskExtensions { 6 | [DebuggerStepThrough] 7 | public static Task WhenAll(this IEnumerable source) => Task.WhenAll(source); 8 | 9 | [DebuggerStepThrough] 10 | public static Task WhenAll(this IEnumerable> source) => Task.WhenAll(source); 11 | } 12 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/EpochExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client; 2 | 3 | static class EpochExtensions { 4 | #if NET 5 | static readonly DateTime UnixEpoch = DateTime.UnixEpoch; 6 | #else 7 | const long TicksPerMillisecond = 10000; 8 | const long TicksPerSecond = TicksPerMillisecond * 1000; 9 | const long TicksPerMinute = TicksPerSecond * 60; 10 | const long TicksPerHour = TicksPerMinute * 60; 11 | const long TicksPerDay = TicksPerHour * 24; 12 | const int DaysPerYear = 365; 13 | const int DaysPer4Years = DaysPerYear * 4 + 1; 14 | const int DaysPer100Years = DaysPer4Years * 25 - 1; 15 | const int DaysPer400Years = DaysPer100Years * 4 + 1; 16 | const int DaysTo1970 = DaysPer400Years * 4 + DaysPer100Years * 3 + DaysPer4Years * 17 + DaysPerYear; 17 | const long UnixEpochTicks = DaysTo1970 * TicksPerDay; 18 | 19 | static readonly DateTime UnixEpoch = new(UnixEpochTicks, DateTimeKind.Utc); 20 | #endif 21 | 22 | public static DateTime FromTicksSinceEpoch(this long value) => new(UnixEpoch.Ticks + value, DateTimeKind.Utc); 23 | 24 | public static long ToTicksSinceEpoch(this DateTime value) => (value - UnixEpoch).Ticks; 25 | } 26 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/MetadataExtensions.cs: -------------------------------------------------------------------------------- 1 | using Grpc.Core; 2 | 3 | namespace KurrentDB.Client; 4 | 5 | static class MetadataExtensions { 6 | public static bool TryGetValue(this Metadata metadata, string key, out string? value) { 7 | value = default; 8 | 9 | foreach (var entry in metadata) { 10 | if (entry.Key != key) 11 | continue; 12 | 13 | value = entry.Value; 14 | return true; 15 | } 16 | 17 | return false; 18 | } 19 | 20 | public static StreamState GetStreamState(this Metadata metadata, string key) => 21 | metadata.TryGetValue(key, out var s) && ulong.TryParse(s, out var value) 22 | ? value 23 | : StreamState.NoStream; 24 | 25 | public static int GetIntValueOrDefault(this Metadata metadata, string key) => 26 | metadata.TryGetValue(key, out var s) && int.TryParse(s, out var value) 27 | ? value 28 | : default; 29 | } 30 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/Shims/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | #if !NET 2 | 3 | using System.ComponentModel; 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace System.Runtime.CompilerServices; 7 | 8 | [EditorBrowsable(EditorBrowsableState.Never)] 9 | class IsExternalInit{} 10 | #endif 11 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Common/Shims/TaskCompletionSource.cs: -------------------------------------------------------------------------------- 1 | #if !NET 2 | // ReSharper disable CheckNamespace 3 | 4 | namespace System.Threading.Tasks; 5 | 6 | class TaskCompletionSource : TaskCompletionSource { 7 | public void SetResult() => base.SetResult(null); 8 | public bool TrySetResult() => base.TrySetResult(null); 9 | } 10 | 11 | #endif -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/DefaultRequestVersionHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace KurrentDB.Client { 7 | internal class DefaultRequestVersionHandler : DelegatingHandler { 8 | public DefaultRequestVersionHandler(HttpMessageHandler innerHandler) : base(innerHandler) { } 9 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { 10 | request.Version = new Version(2, 0); 11 | return base.SendAsync(request, cancellationToken); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/EndPointExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace KurrentDB.Client { 5 | internal static class EndPointExtensions { 6 | public static string GetHost(this EndPoint endpoint) => 7 | endpoint switch { 8 | IPEndPoint ip => ip.Address.ToString(), 9 | DnsEndPoint dns => dns.Host, 10 | _ => throw new ArgumentOutOfRangeException(nameof(endpoint), endpoint?.GetType(), 11 | "An invalid endpoint has been provided") 12 | }; 13 | 14 | public static int GetPort(this EndPoint endpoint) => 15 | endpoint switch { 16 | IPEndPoint ip => ip.Port, 17 | DnsEndPoint dns => dns.Port, 18 | _ => throw new ArgumentOutOfRangeException(nameof(endpoint), endpoint?.GetType(), 19 | "An invalid endpoint has been provided") 20 | }; 21 | 22 | public static Uri ToUri(this EndPoint endPoint, bool https) => new UriBuilder { 23 | Scheme = https ? Uri.UriSchemeHttps : Uri.UriSchemeHttp, 24 | Host = endPoint.GetHost(), 25 | Port = endPoint.GetPort() 26 | }.Uri; 27 | 28 | public static string? ToHttpUrl(this EndPoint endPoint, string schema, string? rawUrl = null) => 29 | endPoint switch { 30 | IPEndPoint ipEndPoint => CreateHttpUrl(schema, ipEndPoint.Address.ToString(), ipEndPoint.Port, 31 | rawUrl != null ? rawUrl.TrimStart('/') : string.Empty), 32 | DnsEndPoint dnsEndpoint => CreateHttpUrl(schema, dnsEndpoint.Host, dnsEndpoint.Port, 33 | rawUrl != null ? rawUrl.TrimStart('/') : string.Empty), 34 | _ => null 35 | }; 36 | 37 | private static string CreateHttpUrl(string schema, string host, int port, string path) { 38 | return $"{schema}://{host}:{port}/{path}"; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/AccessDeniedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KurrentDB.Client { 4 | /// 5 | /// Exception thrown when a user is not authorised to carry out 6 | /// an operation. 7 | /// 8 | public class AccessDeniedException : Exception { 9 | /// 10 | /// Constructs a new . 11 | /// 12 | public AccessDeniedException(string message, Exception innerException) : base(message, innerException) { 13 | } 14 | 15 | /// 16 | /// Constructs a new . 17 | /// 18 | public AccessDeniedException() : base("Access denied.") { 19 | 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/ConnectionString/ConnectionStringParseException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KurrentDB.Client { 4 | /// 5 | /// The base exception that is thrown when an KurrentDB connection string could not be parsed. 6 | /// 7 | public class ConnectionStringParseException : Exception { 8 | /// 9 | /// Constructs a new . 10 | /// 11 | /// 12 | /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. 13 | public ConnectionStringParseException(string message, Exception? innerException = null) : base(message, innerException) { } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/ConnectionString/DuplicateKeyException.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// The exception that is thrown when a key in the KurrentDB connection string is duplicated. 4 | /// 5 | public class DuplicateKeyException : ConnectionStringParseException { 6 | /// 7 | /// Constructs a new . 8 | /// 9 | /// 10 | public DuplicateKeyException(string key) 11 | : base($"Duplicate key: '{key}'") { } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/ConnectionString/InvalidClientCertificateException.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// The exception that is thrown when a certificate is invalid or not found in the KurrentDB connection string. 4 | /// 5 | public class InvalidClientCertificateException : ConnectionStringParseException { 6 | /// 7 | /// Constructs a new . 8 | /// 9 | /// 10 | /// 11 | public InvalidClientCertificateException(string message, Exception? innerException = null) 12 | : base(message, innerException) { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/ConnectionString/InvalidHostException.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// The exception that is thrown when there is an invalid host in the KurrentDB connection string. 4 | /// 5 | public class InvalidHostException : ConnectionStringParseException { 6 | /// 7 | /// Constructs a new . 8 | /// 9 | /// 10 | public InvalidHostException(string host) 11 | : base($"Invalid host: '{host}'") { } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/ConnectionString/InvalidKeyValuePairException.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// The exception that is thrown when an invalid key value pair is found in an KurrentDB connection string. 4 | /// 5 | public class InvalidKeyValuePairException : ConnectionStringParseException { 6 | /// 7 | /// Constructs a new . 8 | /// 9 | /// 10 | public InvalidKeyValuePairException(string keyValuePair) 11 | : base($"Invalid key/value pair: '{keyValuePair}'") { } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/ConnectionString/InvalidSchemeException.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// The exception that is thrown when an invalid scheme is defined in the KurrentDB connection string. 4 | /// 5 | public class InvalidSchemeException : ConnectionStringParseException { 6 | /// 7 | /// Constructs a new . 8 | /// 9 | /// 10 | /// 11 | public InvalidSchemeException(string scheme, string[] supportedSchemes) 12 | : base($"Invalid scheme: '{scheme}'. Supported values are: {string.Join(",", supportedSchemes)}") { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/ConnectionString/InvalidSettingException.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// The exception that is thrown when an invalid setting is found in an KurrentDB connection string. 4 | /// 5 | public class InvalidSettingException : ConnectionStringParseException { 6 | /// 7 | /// Constructs a new . 8 | /// 9 | /// 10 | public InvalidSettingException(string message) : base(message) { } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/ConnectionString/InvalidUserCredentialsException.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// The exception that is thrown when an invalid is specified in the KurrentDB connection string. 4 | /// 5 | public class InvalidUserCredentialsException : ConnectionStringParseException { 6 | /// 7 | /// 8 | /// 9 | /// 10 | public InvalidUserCredentialsException(string userInfo) 11 | : base($"Invalid user credentials: '{userInfo}'. Username & password must be delimited by a colon") { } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/ConnectionString/NoSchemeException.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// The exception that is thrown when no scheme was specified in the KurrentDB connection string. 4 | /// 5 | public class NoSchemeException : ConnectionStringParseException { 6 | /// 7 | /// Constructs a new . 8 | /// 9 | public NoSchemeException() 10 | : base("Could not parse scheme from connection string") { } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/DiscoveryException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KurrentDB.Client { 4 | /// 5 | /// The exception that is thrown when discovery fails. 6 | /// 7 | public class DiscoveryException : Exception { 8 | /// 9 | /// The configured number of discovery attempts. 10 | /// 11 | public int MaxDiscoverAttempts { get; } 12 | 13 | /// 14 | /// Constructs a new . 15 | /// 16 | /// 17 | /// 18 | [Obsolete] 19 | public DiscoveryException(string message, Exception? innerException = null) 20 | : base(message, innerException) { 21 | MaxDiscoverAttempts = 0; 22 | } 23 | 24 | /// 25 | /// Constructs a new . 26 | /// 27 | /// The configured number of discovery attempts. 28 | public DiscoveryException(int maxDiscoverAttempts) : base( 29 | $"Failed to discover candidate in {maxDiscoverAttempts} attempts.") { 30 | MaxDiscoverAttempts = maxDiscoverAttempts; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/NotAuthenticatedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KurrentDB.Client { 4 | /// 5 | /// The exception that is thrown when a user is not authenticated. 6 | /// 7 | public class NotAuthenticatedException : Exception { 8 | /// 9 | /// Constructs a new . 10 | /// 11 | /// 12 | /// 13 | public NotAuthenticatedException(string message, Exception? innerException = null) : base(message, innerException) { 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/NotLeaderException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace KurrentDB.Client { 5 | /// 6 | /// The exception that is thrown when an operation requiring a leader node is made on a follower node. 7 | /// 8 | public class NotLeaderException : Exception { 9 | 10 | /// 11 | /// The of the current leader node. 12 | /// 13 | public DnsEndPoint LeaderEndpoint { get; } 14 | 15 | /// 16 | /// Constructs a new 17 | /// 18 | /// 19 | /// 20 | /// 21 | public NotLeaderException(string host, int port, Exception? exception = null) : base( 22 | $"Not leader. New leader at {host}:{port}.", exception) { 23 | LeaderEndpoint = new DnsEndPoint(host, port); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/RequiredMetadataPropertyMissingException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KurrentDB.Client { 4 | /// 5 | /// Exception thrown when a required metadata property is missing. 6 | /// 7 | public class RequiredMetadataPropertyMissingException : Exception { 8 | /// 9 | /// Constructs a new . 10 | /// 11 | /// 12 | /// 13 | public RequiredMetadataPropertyMissingException(string missingMetadataProperty, 14 | Exception? innerException = null) : 15 | base($"Required metadata property {missingMetadataProperty} is missing", innerException) { 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/ScavengeNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KurrentDB.Client { 4 | /// 5 | /// The exception that is thrown when attempting to see the status of a scavenge operation that does not exist. 6 | /// 7 | public class ScavengeNotFoundException : Exception { 8 | /// 9 | /// The id of the scavenge operation. 10 | /// 11 | public string? ScavengeId { get; } 12 | 13 | /// 14 | /// Constructs a new . 15 | /// 16 | /// 17 | /// 18 | public ScavengeNotFoundException(string? scavengeId, Exception? exception = null) : base( 19 | $"Scavenge not found. The currently running scavenge is {scavengeId ?? ""}.", exception) { 20 | ScavengeId = scavengeId; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/StreamDeletedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KurrentDB.Client { 4 | /// 5 | /// Exception thrown if an operation is attempted on a stream which 6 | /// has been deleted. 7 | /// 8 | public class StreamDeletedException : Exception { 9 | /// 10 | /// The name of the deleted stream. 11 | /// 12 | public readonly string Stream; 13 | 14 | /// 15 | /// Constructs a new instance of . 16 | /// 17 | /// The name of the deleted stream. 18 | /// 19 | public StreamDeletedException(string stream, Exception? exception = null) 20 | : base($"Event stream '{stream}' is deleted.", exception) { 21 | Stream = stream; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/StreamNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KurrentDB.Client { 4 | /// 5 | /// The exception that is thrown when an attempt is made to read or write to a stream that does not exist. 6 | /// 7 | public class StreamNotFoundException : Exception { 8 | /// 9 | /// The name of the stream. 10 | /// 11 | public readonly string Stream; 12 | 13 | /// 14 | /// Constructs a new instance of . 15 | /// 16 | /// The name of the stream. 17 | /// 18 | public StreamNotFoundException(string stream, Exception? exception = null) 19 | : base($"Event stream '{stream}' was not found.", exception) { 20 | Stream = stream; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Exceptions/UserNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KurrentDB.Client { 4 | /// 5 | /// The exception that is thrown when an operation is performed on an internal user that does not exist. 6 | /// 7 | public class UserNotFoundException : Exception { 8 | /// 9 | /// The login name of the user. 10 | /// 11 | public string LoginName { get; } 12 | 13 | /// 14 | /// Constructs a new . 15 | /// 16 | /// 17 | /// 18 | public UserNotFoundException(string loginName, Exception? exception = null) 19 | : base($"User '{loginName}' was not found.", exception) { 20 | LoginName = loginName; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/GrpcGossipClient.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using EventStore.Client; 3 | using EventStore.Client.Gossip; 4 | using Grpc.Core; 5 | 6 | namespace KurrentDB.Client { 7 | internal class GrpcGossipClient : IGossipClient { 8 | private readonly KurrentDBClientSettings _settings; 9 | 10 | public GrpcGossipClient(KurrentDBClientSettings settings) { 11 | _settings = settings; 12 | } 13 | 14 | public async ValueTask GetAsync(ChannelBase channel, CancellationToken ct) { 15 | var client = new Gossip.GossipClient(channel); 16 | using var call = client.ReadAsync( 17 | new Empty(), 18 | KurrentDBCallOptions.CreateNonStreaming(_settings, ct)); 19 | var result = await call.ResponseAsync.ConfigureAwait(false); 20 | 21 | return new(result.Members.Select(x => 22 | new ClusterMessages.MemberInfo( 23 | Uuid.FromDto(x.InstanceId), 24 | (ClusterMessages.VNodeState)x.State, 25 | x.IsAlive, 26 | new DnsEndPoint(x.HttpEndPoint.Address, (int)x.HttpEndPoint.Port))).ToArray()); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/HashCode.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace KurrentDB.Client { 5 | #pragma warning disable 1591 6 | public readonly struct HashCode { 7 | private readonly int _value; 8 | 9 | private HashCode(int value) { 10 | _value = value; 11 | } 12 | 13 | public static readonly HashCode Hash = default; 14 | 15 | public HashCode Combine(T? value) where T : struct => Combine(value ?? default); 16 | 17 | public HashCode Combine(T value) where T: struct { 18 | unchecked { 19 | return new HashCode((_value * 397) ^ value.GetHashCode()); 20 | } 21 | } 22 | 23 | public HashCode Combine(string? value){ 24 | unchecked { 25 | return new HashCode((_value * 397) ^ (value?.GetHashCode() ?? 0)); 26 | } 27 | } 28 | 29 | public HashCode Combine(IEnumerable? values) where T: struct => 30 | (values ?? Enumerable.Empty()).Aggregate(Hash, (previous, value) => previous.Combine(value)); 31 | 32 | public HashCode Combine(IEnumerable? values) => 33 | (values ?? Enumerable.Empty()).Aggregate(Hash, (previous, value) => previous.Combine(value)); 34 | 35 | public static implicit operator int(HashCode value) => value._value; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/IChannelSelector.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Grpc.Core; 5 | 6 | namespace KurrentDB.Client { 7 | internal interface IChannelSelector { 8 | // Let the channel selector pick an endpoint. 9 | Task SelectChannelAsync(CancellationToken cancellationToken); 10 | 11 | // Get a channel for the specified endpoint 12 | ChannelBase SelectChannel(DnsEndPoint endPoint); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/IEventFilter.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// An interface that represents a search filter, used for read operations. 4 | /// 5 | public interface IEventFilter { 6 | /// 7 | /// The s associated with this . 8 | /// 9 | PrefixFilterExpression[] Prefixes { get; } 10 | 11 | /// 12 | /// The associated with this . 13 | /// 14 | RegularFilterExpression Regex { get; } 15 | 16 | /// 17 | /// The maximum number of events to read that do not match the filter before the operation returns. 18 | /// 19 | uint? MaxSearchWindow { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/IGossipClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Grpc.Core; 4 | 5 | namespace KurrentDB.Client { 6 | internal interface IGossipClient { 7 | public ValueTask GetAsync(ChannelBase channel, 8 | CancellationToken cancellationToken); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/IPosition.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// Represents the position in a stream or transaction file 4 | /// 5 | public interface IPosition { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/IServerCapabilitiesClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Grpc.Core; 4 | 5 | namespace KurrentDB.Client { 6 | internal interface IServerCapabilitiesClient { 7 | public Task GetAsync(CallInvoker callInvoker, CancellationToken cancellationToken); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/KurrentDBClientOperationOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace KurrentDB.Client { 6 | /// 7 | /// A class representing the options to apply to an individual operation. 8 | /// 9 | public class KurrentDBClientOperationOptions { 10 | /// 11 | /// Whether or not to immediately throw a when an append fails. 12 | /// 13 | public bool ThrowOnAppendFailure { get; set; } 14 | 15 | /// 16 | /// The batch size, in bytes. 17 | /// 18 | public int BatchAppendSize { get; set; } 19 | 20 | /// 21 | /// A callback function to extract the authorize header value from the used in the operation. 22 | /// 23 | public Func> GetAuthenticationHeaderValue { get; set; } = 24 | null!; 25 | 26 | /// 27 | /// The default . 28 | /// 29 | public static KurrentDBClientOperationOptions Default => new() { 30 | ThrowOnAppendFailure = true, 31 | GetAuthenticationHeaderValue = (userCredentials, _) => new ValueTask(userCredentials.ToString()), 32 | BatchAppendSize = 3 * 1024 * 1024 33 | }; 34 | 35 | 36 | /// 37 | /// Clones a copy of the current . 38 | /// 39 | /// 40 | public KurrentDBClientOperationOptions Clone() => new() { 41 | ThrowOnAppendFailure = ThrowOnAppendFailure, 42 | GetAuthenticationHeaderValue = GetAuthenticationHeaderValue, 43 | BatchAppendSize = BatchAppendSize 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/NodePreference.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// Indicates the preferred KurrentDB node type to connect to. 4 | /// 5 | public enum NodePreference { 6 | /// 7 | /// When attempting connection, prefers leader node. 8 | /// 9 | Leader, 10 | 11 | /// 12 | /// When attempting connection, prefers follower node. 13 | /// 14 | Follower, 15 | 16 | /// 17 | /// When attempting connection, has no node preference. 18 | /// 19 | Random, 20 | 21 | /// 22 | /// When attempting connection, prefers read only replicas. 23 | /// 24 | ReadOnlyReplica 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/OperationOptions.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client; 2 | 3 | /// 4 | /// A class representing the options to apply to an individual operation. 5 | /// 6 | public class OperationOptions { 7 | 8 | /// 9 | /// Maximum time that the operation will be run 10 | /// 11 | public TimeSpan? Deadline { get; set; } 12 | 13 | /// 14 | /// The for the operation. 15 | /// 16 | public UserCredentials? UserCredentials { get; set; } 17 | 18 | /// 19 | /// Clones a copy of the current . 20 | /// 21 | /// 22 | public OperationOptions With(KurrentDBClientSettings clientSettings) { 23 | Deadline ??= clientSettings.DefaultDeadline; 24 | UserCredentials ??= clientSettings.DefaultCredentials; 25 | 26 | return this; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/ReconnectionRequired.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace KurrentDB.Client { 4 | internal abstract record ReconnectionRequired { 5 | public record None : ReconnectionRequired { 6 | public static None Instance = new(); 7 | } 8 | 9 | public record Rediscover : ReconnectionRequired { 10 | public static Rediscover Instance = new(); 11 | } 12 | 13 | public record NewLeader(DnsEndPoint EndPoint) : ReconnectionRequired; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Serialization/ISerializer.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client.Core.Serialization; 2 | 3 | /// 4 | /// Defines the core serialization capabilities required by the KurrentDB client. 5 | /// Implementations of this interface handle the conversion between .NET objects and their 6 | /// binary representation for storage in and retrieval from the event store. 7 | ///
8 | /// The client ships default System.Text.Json implementation, but custom implementations can be provided or other formats. 9 | ///
10 | public interface ISerializer { 11 | /// 12 | /// Converts a .NET object to its binary representation for storage in the event store. 13 | /// 14 | /// The object to serialize. This could be an event, command, or metadata object. 15 | /// 16 | /// A binary representation of the object that can be stored in KurrentDB. 17 | /// 18 | public ReadOnlyMemory Serialize(object value); 19 | 20 | /// 21 | /// Reconstructs a .NET object from its binary representation retrieved from the event store. 22 | /// 23 | /// The binary data to deserialize, typically retrieved from a KurrentDB event. 24 | /// The target .NET type to deserialize the data into, determined from message type mappings. 25 | /// 26 | /// The deserialized object cast to the specified type, or null if the data cannot be deserialized. 27 | /// The returned object will be an instance of the specified type or a compatible subtype. 28 | /// 29 | public object? Deserialize(ReadOnlyMemory data, Type type); 30 | } 31 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Serialization/SystemTextJsonSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace KurrentDB.Client.Core.Serialization; 5 | 6 | public class SystemTextJsonSerializationSettings { 7 | public static readonly JsonSerializerOptions DefaultJsonSerializerOptions = 8 | new JsonSerializerOptions(JsonSerializerOptions.Default) { 9 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 10 | DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, 11 | PropertyNameCaseInsensitive = false, 12 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 13 | UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode, 14 | UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, 15 | NumberHandling = JsonNumberHandling.AllowReadingFromString, 16 | Converters = { 17 | new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), 18 | } 19 | }; 20 | 21 | public JsonSerializerOptions Options { get; set; } = DefaultJsonSerializerOptions; 22 | } 23 | 24 | public class SystemTextJsonSerializer(SystemTextJsonSerializationSettings? options = null) : ISerializer { 25 | readonly JsonSerializerOptions _options = options?.Options ?? SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions; 26 | 27 | public ReadOnlyMemory Serialize(object value) => 28 | JsonSerializer.SerializeToUtf8Bytes(value, _options); 29 | 30 | public object? Deserialize(ReadOnlyMemory data, Type type) => 31 | !data.IsEmpty ? JsonSerializer.Deserialize(data.Span, type, _options) : null; 32 | } 33 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/Serialization/TypeProvider.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client.Core.Serialization; 2 | 3 | static class TypeProvider { 4 | public static Type? GetTypeByFullName(string fullName) => 5 | Type.GetType(fullName) ?? GetFirstMatchingTypeFromCurrentDomainAssembly(fullName); 6 | 7 | static Type? GetFirstMatchingTypeFromCurrentDomainAssembly(string fullName) { 8 | var firstNamespacePart = fullName.Split('.')[0]; 9 | 10 | return AppDomain.CurrentDomain.GetAssemblies() 11 | .OrderByDescending(assembly => assembly.FullName?.StartsWith(firstNamespacePart) == true) 12 | .Select(assembly => assembly.GetType(fullName)) 13 | .FirstOrDefault(type => type != null); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/ServerCapabilities.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | #pragma warning disable 1591 3 | public record ServerCapabilities( 4 | bool SupportsBatchAppend = false, 5 | bool SupportsPersistentSubscriptionsToAll = false, 6 | bool SupportsPersistentSubscriptionsGetInfo = false, 7 | bool SupportsPersistentSubscriptionsRestartSubsystem = false, 8 | bool SupportsPersistentSubscriptionsReplayParked = false, 9 | bool SupportsPersistentSubscriptionsList = false); 10 | } 11 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/SingleNodeChannelSelector.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Grpc.Core; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Extensions.Logging.Abstractions; 7 | 8 | namespace KurrentDB.Client { 9 | internal class SingleNodeChannelSelector : IChannelSelector { 10 | private readonly ILogger _log; 11 | private readonly ChannelCache _channelCache; 12 | private readonly DnsEndPoint _endPoint; 13 | 14 | public SingleNodeChannelSelector( 15 | KurrentDBClientSettings settings, 16 | ChannelCache channelCache) { 17 | 18 | _log = settings.LoggerFactory?.CreateLogger() ?? 19 | new NullLogger(); 20 | 21 | _channelCache = channelCache; 22 | 23 | var uri = settings.ConnectivitySettings.ResolvedAddressOrDefault; 24 | _endPoint = new DnsEndPoint(host: uri.Host, port: uri.Port); 25 | } 26 | 27 | public Task SelectChannelAsync(CancellationToken cancellationToken) => 28 | Task.FromResult(SelectChannel(_endPoint)); 29 | 30 | public ChannelBase SelectChannel(DnsEndPoint endPoint) { 31 | _log.LogInformation("Selected {endPoint}.", endPoint); 32 | 33 | return _channelCache.GetChannelInfo(endPoint); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/SingleNodeHttpHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace KurrentDB.Client { 7 | internal class SingleNodeHttpHandler : DelegatingHandler { 8 | private readonly KurrentDBClientSettings _settings; 9 | 10 | public SingleNodeHttpHandler(KurrentDBClientSettings settings) { 11 | _settings = settings; 12 | } 13 | 14 | protected override Task SendAsync(HttpRequestMessage request, 15 | CancellationToken cancellationToken) { 16 | request.RequestUri = new UriBuilder(request.RequestUri!) { 17 | Scheme = _settings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme 18 | }.Uri; 19 | return base.SendAsync(request, cancellationToken); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/StreamIdentifier.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Google.Protobuf; 3 | 4 | namespace EventStore.Client { 5 | #pragma warning disable 1591 6 | internal partial class StreamIdentifier { 7 | private string? _cached; 8 | 9 | public static implicit operator string?(StreamIdentifier? source) { 10 | if (source == null) { 11 | return null; 12 | } 13 | if (source._cached != null || source.StreamName.IsEmpty) return source._cached; 14 | 15 | #if NET 16 | var tmp = Encoding.UTF8.GetString(source.StreamName.Span); 17 | #else 18 | var tmp = Encoding.UTF8.GetString(source.StreamName.ToByteArray()); 19 | #endif 20 | //this doesn't have to be thread safe, its just a cache in case the identifier is turned into a string several times 21 | source._cached = tmp; 22 | return source._cached; 23 | } 24 | 25 | public static implicit operator StreamIdentifier(string source) => 26 | new() {StreamName = ByteString.CopyFromUtf8(source)}; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/SubscriptionDroppedReason.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// Represents the reason subscription was dropped. 4 | /// 5 | public enum SubscriptionDroppedReason { 6 | /// 7 | /// Subscription was dropped because the subscription was disposed. 8 | /// 9 | Disposed, 10 | /// 11 | /// Subscription was dropped because of an error in user code. 12 | /// 13 | SubscriberError, 14 | /// 15 | /// Subscription was dropped because of a server error. 16 | /// 17 | ServerError 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/SystemRoles.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// Roles used by the system. 4 | /// 5 | public static class SystemRoles { 6 | /// 7 | /// The $admins role. 8 | /// 9 | public const string Admins = "$admins"; 10 | 11 | /// 12 | /// The $ops role. 13 | /// 14 | public const string Operations = "$ops"; 15 | 16 | /// 17 | /// The $all role. 18 | /// 19 | public const string All = "$all"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/TaskExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace KurrentDB.Client { 5 | internal static class TaskExtensions { 6 | // To give up waiting for the task, cancel the token. 7 | // obvs this wouldn't cancel the task itself. 8 | public static async ValueTask WithCancellation(this Task task, CancellationToken cancellationToken) { 9 | if (task.Status == TaskStatus.RanToCompletion) 10 | return task.Result; 11 | 12 | await Task 13 | .WhenAny( 14 | task, 15 | Task.Delay(-1, cancellationToken)) 16 | .ConfigureAwait(false); 17 | 18 | cancellationToken.ThrowIfCancellationRequested(); 19 | return await task.ConfigureAwait(false); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/UserCredentials.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using System.Text; 3 | using static System.Convert; 4 | 5 | namespace KurrentDB.Client { 6 | /// 7 | /// Represents either a username/password pair or a JWT token used for authentication and 8 | /// authorization to perform operations on the KurrentDB. 9 | /// 10 | public class UserCredentials { 11 | // ReSharper disable once InconsistentNaming 12 | static readonly UTF8Encoding UTF8NoBom = new UTF8Encoding(false); 13 | 14 | /// 15 | /// Constructs a new . 16 | /// 17 | public UserCredentials(string username, string password) { 18 | Username = username; 19 | Password = password; 20 | 21 | Authorization = new( 22 | Constants.Headers.BasicScheme, 23 | ToBase64String(UTF8NoBom.GetBytes($"{username}:{password}")) 24 | ); 25 | } 26 | 27 | /// 28 | /// Constructs a new . 29 | /// 30 | public UserCredentials(string bearerToken) { 31 | Authorization = new(Constants.Headers.BearerScheme, bearerToken); 32 | } 33 | 34 | AuthenticationHeaderValue Authorization { get; } 35 | 36 | /// 37 | /// The username 38 | /// 39 | public string? Username { get; } 40 | 41 | /// 42 | /// The password 43 | /// 44 | public string? Password { get; } 45 | 46 | /// 47 | public override string ToString() => Authorization.ToString(); 48 | 49 | /// 50 | /// Implicitly convert a to a . 51 | /// 52 | public static implicit operator string(UserCredentials self) => self.ToString(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/protos/gossip.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package event_store.client.gossip; 3 | option java_package = "io.kurrent.client.gossip"; 4 | 5 | import "shared.proto"; 6 | 7 | service Gossip { 8 | rpc Read (event_store.client.Empty) returns (ClusterInfo); 9 | } 10 | 11 | message ClusterInfo { 12 | repeated MemberInfo members = 1; 13 | } 14 | 15 | message EndPoint { 16 | string address = 1; 17 | uint32 port = 2; 18 | } 19 | 20 | message MemberInfo { 21 | enum VNodeState { 22 | Initializing = 0; 23 | DiscoverLeader = 1; 24 | Unknown = 2; 25 | PreReplica = 3; 26 | CatchingUp = 4; 27 | Clone = 5; 28 | Follower = 6; 29 | PreLeader = 7; 30 | Leader = 8; 31 | Manager = 9; 32 | ShuttingDown = 10; 33 | Shutdown = 11; 34 | ReadOnlyLeaderless = 12; 35 | PreReadOnlyReplica = 13; 36 | ReadOnlyReplica = 14; 37 | ResigningLeader = 15; 38 | } 39 | event_store.client.UUID instance_id = 1; 40 | int64 time_stamp = 2; 41 | VNodeState state = 3; 42 | bool is_alive = 4; 43 | EndPoint http_end_point = 5; 44 | } 45 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/protos/operations.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package event_store.client.operations; 3 | option java_package = "io.kurrent.client.operations"; 4 | 5 | import "shared.proto"; 6 | 7 | service Operations { 8 | rpc StartScavenge (StartScavengeReq) returns (ScavengeResp); 9 | rpc StopScavenge (StopScavengeReq) returns (ScavengeResp); 10 | rpc Shutdown (Empty) returns (Empty); 11 | rpc MergeIndexes (Empty) returns (Empty); 12 | rpc ResignNode (Empty) returns (Empty); 13 | rpc SetNodePriority (SetNodePriorityReq) returns (Empty); 14 | rpc RestartPersistentSubscriptions (Empty) returns (Empty); 15 | } 16 | 17 | message StartScavengeReq { 18 | Options options = 1; 19 | message Options { 20 | int32 thread_count = 1; 21 | int32 start_from_chunk = 2; 22 | } 23 | } 24 | 25 | message StopScavengeReq { 26 | Options options = 1; 27 | message Options { 28 | string scavenge_id = 1; 29 | } 30 | } 31 | 32 | message ScavengeResp { 33 | string scavenge_id = 1; 34 | ScavengeResult scavenge_result = 2; 35 | 36 | enum ScavengeResult { 37 | Started = 0; 38 | InProgress = 1; 39 | Stopped = 2; 40 | } 41 | } 42 | 43 | message SetNodePriorityReq { 44 | int32 priority = 1; 45 | } 46 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/protos/serverfeatures.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package event_store.client.server_features; 3 | option java_package = "io.kurrent.dbclient.proto.serverfeatures"; 4 | import "shared.proto"; 5 | 6 | service ServerFeatures { 7 | rpc GetSupportedMethods (event_store.client.Empty) returns (SupportedMethods); 8 | } 9 | 10 | message SupportedMethods { 11 | repeated SupportedMethod methods = 1; 12 | string event_store_server_version = 2; 13 | } 14 | 15 | message SupportedMethod { 16 | string method_name = 1; 17 | string service_name = 2; 18 | repeated string features = 3; 19 | } 20 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Core/protos/shared.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package event_store.client; 3 | option java_package = "io.kurrent.dbclient.proto.shared"; 4 | import "google/protobuf/empty.proto"; 5 | 6 | message UUID { 7 | oneof value { 8 | Structured structured = 1; 9 | string string = 2; 10 | } 11 | 12 | message Structured { 13 | int64 most_significant_bits = 1; 14 | int64 least_significant_bits = 2; 15 | } 16 | } 17 | message Empty { 18 | } 19 | 20 | message StreamIdentifier { 21 | reserved 1 to 2; 22 | bytes stream_name = 3; 23 | } 24 | 25 | message AllStreamPosition { 26 | uint64 commit_position = 1; 27 | uint64 prepare_position = 2; 28 | } 29 | 30 | message WrongExpectedVersion { 31 | oneof current_stream_revision_option { 32 | uint64 current_stream_revision = 1; 33 | google.protobuf.Empty current_no_stream = 2; 34 | } 35 | oneof expected_stream_position_option { 36 | uint64 expected_stream_position = 3; 37 | google.protobuf.Empty expected_any = 4; 38 | google.protobuf.Empty expected_stream_exists = 5; 39 | google.protobuf.Empty expected_no_stream = 6; 40 | } 41 | } 42 | 43 | message AccessDenied {} 44 | 45 | message StreamDeleted { 46 | StreamIdentifier stream_identifier = 1; 47 | } 48 | 49 | message Timeout {} 50 | 51 | message Unknown {} 52 | 53 | message InvalidTransaction {} 54 | 55 | message MaximumAppendSizeExceeded { 56 | uint32 maxAppendSize = 1; 57 | } 58 | 59 | message BadRequest { 60 | string message = 1; 61 | } 62 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/OpenTelemetry/TracerProviderBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Diagnostics; 2 | using JetBrains.Annotations; 3 | using OpenTelemetry.Trace; 4 | 5 | namespace EventStore.Client.Extensions.OpenTelemetry; 6 | 7 | /// 8 | /// Extension methods used to facilitate tracing instrumentation of the EventStore Client. 9 | /// 10 | [PublicAPI] 11 | public static class TracerProviderBuilderExtensions { 12 | /// 13 | /// Adds the EventStore client ActivitySource name to the list of subscribed sources on the 14 | /// 15 | /// being configured. 16 | /// The instance of to chain configuration. 17 | public static TracerProviderBuilder AddKurrentDBClientInstrumentation(this TracerProviderBuilder builder) => 18 | builder.AddSource(KurrentDBClientDiagnostics.InstrumentationName); 19 | } 20 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Operations/KurrentDBOperationsClient.cs: -------------------------------------------------------------------------------- 1 | using Grpc.Core; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Logging.Abstractions; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace KurrentDB.Client; 7 | 8 | /// 9 | /// The client used to perform maintenance and other administrative tasks on the KurrentDB. 10 | /// 11 | public sealed partial class KurrentDBOperationsClient : KurrentDBClientBase { 12 | static readonly Dictionary> ExceptionMap = 13 | new() { 14 | [Constants.Exceptions.ScavengeNotFound] = ex => new ScavengeNotFoundException( 15 | ex.Trailers.FirstOrDefault(x => x.Key == Constants.Exceptions.ScavengeId)?.Value 16 | ) 17 | }; 18 | 19 | readonly ILogger _log; 20 | 21 | /// 22 | /// Constructs a new . This method is not intended to be called directly in your code. 23 | /// 24 | /// 25 | public KurrentDBOperationsClient(IOptions options) : this(options.Value) { } 26 | 27 | /// 28 | /// Constructs a new . 29 | /// 30 | /// 31 | public KurrentDBOperationsClient(KurrentDBClientSettings? settings = null) : base(settings, ExceptionMap) => 32 | _log = Settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); 33 | } 34 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Operations/ScavengeResult.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// An enumeration that represents the result of a scavenge operation. 4 | /// 5 | public enum ScavengeResult { 6 | /// 7 | /// The scavenge operation has started. 8 | /// 9 | Started, 10 | /// 11 | /// The scavenge operation is in progress. 12 | /// 13 | InProgress, 14 | 15 | /// 16 | /// The scavenge operation has stopped. 17 | /// 18 | Stopped, 19 | 20 | /// 21 | /// The status of the scavenge operation was unknown. 22 | /// 23 | Unknown 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/PersistentSubscriptions/KurrentDBPersistentSubscriptionsClient.RestartSubsystem.cs: -------------------------------------------------------------------------------- 1 | using EventStore.Client; 2 | using EventStore.Client.PersistentSubscriptions; 3 | 4 | #nullable enable 5 | namespace KurrentDB.Client { 6 | partial class KurrentDBPersistentSubscriptionsClient { 7 | /// 8 | /// Restarts the persistent subscriptions subsystem. 9 | /// 10 | public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, 11 | CancellationToken cancellationToken = default) { 12 | 13 | var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); 14 | if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsRestartSubsystem) { 15 | await new PersistentSubscriptions.PersistentSubscriptionsClient(channelInfo.CallInvoker) 16 | .RestartSubsystemAsync(new Empty(), KurrentDBCallOptions 17 | .CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) 18 | .ConfigureAwait(false); 19 | return; 20 | } 21 | 22 | await HttpPost( 23 | path: "/subscriptions/restart", 24 | query: "", 25 | onNotFound: () => 26 | throw new Exception("Unexpected exception while restarting the persistent subscription subsystem."), 27 | channelInfo, deadline, userCredentials, cancellationToken) 28 | .ConfigureAwait(false); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/PersistentSubscriptions/MaximumSubscribersReachedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KurrentDB.Client { 4 | /// 5 | /// The exception that is thrown when the maximum number of subscribers on a persistent subscription is exceeded. 6 | /// 7 | public class MaximumSubscribersReachedException : Exception { 8 | /// 9 | /// The stream name. 10 | /// 11 | public readonly string StreamName; 12 | /// 13 | /// The group name. 14 | /// 15 | public readonly string GroupName; 16 | 17 | /// 18 | /// Constructs a new . 19 | /// 20 | /// 21 | public MaximumSubscribersReachedException(string streamName, string groupName, Exception? exception = null) 22 | : base($"Maximum subscriptions reached for subscription group '{groupName}' on stream '{streamName}.'", 23 | exception) { 24 | if (streamName == null) throw new ArgumentNullException(nameof(streamName)); 25 | if (groupName == null) throw new ArgumentNullException(nameof(groupName)); 26 | StreamName = streamName; 27 | GroupName = groupName; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KurrentDB.Client { 4 | /// 5 | /// The exception that is thrown when the KurrentDB drops a persistent subscription. 6 | /// 7 | public class PersistentSubscriptionDroppedByServerException : Exception { 8 | /// 9 | /// The stream name. 10 | /// 11 | public readonly string StreamName; 12 | 13 | /// 14 | /// The group name. 15 | /// 16 | public readonly string GroupName; 17 | 18 | /// 19 | /// Constructs a new . 20 | /// 21 | /// 22 | public PersistentSubscriptionDroppedByServerException(string streamName, string groupName, 23 | Exception? exception = null) 24 | : base($"Subscription group '{groupName}' on stream '{streamName}' was dropped.", exception) { 25 | if (streamName == null) throw new ArgumentNullException(nameof(streamName)); 26 | if (groupName == null) throw new ArgumentNullException(nameof(groupName)); 27 | StreamName = streamName; 28 | GroupName = groupName; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client; 2 | 3 | /// 4 | /// Provides the definitions of the available extra statistics. 5 | /// 6 | public static class PersistentSubscriptionExtraStatistic { 7 | #pragma warning disable CS1591 8 | public const string Highest = "Highest"; 9 | public const string Mean = "Mean"; 10 | public const string Median = "Median"; 11 | public const string Fastest = "Fastest"; 12 | public const string Quintile1 = "Quintile 1"; 13 | public const string Quintile2 = "Quintile 2"; 14 | public const string Quintile3 = "Quintile 3"; 15 | public const string Quintile4 = "Quintile 4"; 16 | public const string Quintile5 = "Quintile 5"; 17 | public const string NinetyPercent = "90%"; 18 | public const string NinetyFivePercent = "95%"; 19 | public const string NinetyNinePercent = "99%"; 20 | public const string NinetyNinePointFivePercent = "99.5%"; 21 | public const string NinetyNinePointNinePercent = "99.9%"; 22 | #pragma warning restore CS1591 23 | } 24 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/PersistentSubscriptions/PersistentSubscriptionMessage.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// The base record of all stream messages. 4 | /// 5 | public abstract record PersistentSubscriptionMessage { 6 | /// 7 | /// A that represents a . 8 | /// 9 | /// The . 10 | /// The number of times the has been retried. 11 | public record Event(ResolvedEvent ResolvedEvent, int? RetryCount) : PersistentSubscriptionMessage; 12 | 13 | /// 14 | /// A representing a stream that was not found. 15 | /// 16 | public record NotFound : PersistentSubscriptionMessage { 17 | internal static readonly NotFound Instance = new(); 18 | } 19 | 20 | /// 21 | /// A indicating that the subscription is ready to send additional messages. 22 | /// 23 | /// The unique identifier of the subscription. 24 | public record SubscriptionConfirmation(string SubscriptionId) : PersistentSubscriptionMessage; 25 | 26 | /// 27 | /// A that could not be identified, usually indicating a lower client compatibility level than the server supports. 28 | /// 29 | public record Unknown : PersistentSubscriptionMessage { 30 | internal static readonly Unknown Instance = new(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// Actions to be taken by server in the case of a client NAK 4 | /// 5 | public enum PersistentSubscriptionNakEventAction { 6 | /// 7 | /// Client unknown on action. Let server decide 8 | /// 9 | Unknown = 0, 10 | 11 | /// 12 | /// Park message do not resend. Put on poison queue 13 | /// 14 | Park = 1, 15 | 16 | /// 17 | /// Explicitly retry the message. 18 | /// 19 | Retry = 2, 20 | 21 | /// 22 | /// Skip this message do not resend do not put in poison queue 23 | /// 24 | Skip = 3, 25 | 26 | /// 27 | /// Stop the subscription. 28 | /// 29 | Stop = 4 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KurrentDB.Client { 4 | /// 5 | /// The exception that is thrown when a persistent subscription is not found. 6 | /// 7 | public class PersistentSubscriptionNotFoundException : Exception { 8 | /// 9 | /// The stream name. 10 | /// 11 | public readonly string StreamName; 12 | /// 13 | /// The group name. 14 | /// 15 | public readonly string GroupName; 16 | 17 | /// 18 | /// Constructs a new . 19 | /// 20 | /// 21 | public PersistentSubscriptionNotFoundException(string streamName, string groupName, Exception? exception = null) 22 | : base($"Subscription group '{groupName}' on stream '{streamName}' does not exist.", exception) { 23 | if (streamName == null) throw new ArgumentNullException(nameof(streamName)); 24 | if (groupName == null) throw new ArgumentNullException(nameof(groupName)); 25 | StreamName = streamName; 26 | GroupName = groupName; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/PersistentSubscriptions/SystemConsumerStrategies.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// System supported consumer strategies for use with persistent subscriptions. 4 | /// 5 | public static class SystemConsumerStrategies { 6 | /// 7 | /// Distributes events to a single client until it is full. Then round robin to the next client. 8 | /// 9 | public const string DispatchToSingle = nameof(DispatchToSingle); 10 | 11 | /// 12 | /// Distribute events to each client in a round robin fashion. 13 | /// 14 | public const string RoundRobin = nameof(RoundRobin); 15 | 16 | /// 17 | /// Distribute events of the same streamId to the same client until it disconnects on a best efforts basis. 18 | /// Designed to be used with indexes such as the category projection. 19 | /// 20 | public const string Pinned = nameof(Pinned); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.Update.cs: -------------------------------------------------------------------------------- 1 | using EventStore.Client; 2 | using EventStore.Client.Projections; 3 | 4 | namespace KurrentDB.Client { 5 | public partial class KurrentDBProjectionManagementClient { 6 | /// 7 | /// Updates a projection. 8 | /// 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | public async Task UpdateAsync(string name, string query, bool? emitEnabled = null, 17 | TimeSpan? deadline = null, UserCredentials? userCredentials = null, 18 | CancellationToken cancellationToken = default) { 19 | var options = new UpdateReq.Types.Options { 20 | Name = name, 21 | Query = query 22 | }; 23 | if (emitEnabled.HasValue) { 24 | options.EmitEnabled = emitEnabled.Value; 25 | } else { 26 | options.NoEmitOptions = new Empty(); 27 | } 28 | 29 | var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); 30 | using var call = new Projections.ProjectionsClient( 31 | channelInfo.CallInvoker).UpdateAsync(new UpdateReq { 32 | Options = options 33 | }, KurrentDBCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); 34 | 35 | await call.ResponseAsync.ConfigureAwait(false); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/ProjectionManagement/KurrentDBProjectionManagementClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Grpc.Core; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Logging.Abstractions; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace KurrentDB.Client { 9 | /// 10 | ///The client used to manage projections on the KurrentDB. 11 | /// 12 | public sealed partial class KurrentDBProjectionManagementClient : KurrentDBClientBase { 13 | private readonly ILogger _log; 14 | 15 | /// 16 | /// Constructs a new . This method is not intended to be called directly from your code. 17 | /// 18 | /// 19 | public KurrentDBProjectionManagementClient(IOptions options) : this(options.Value) { 20 | } 21 | 22 | /// 23 | /// Constructs a new . 24 | /// 25 | /// 26 | public KurrentDBProjectionManagementClient(KurrentDBClientSettings? settings) : base(settings, 27 | new Dictionary>()) { 28 | _log = settings?.LoggerFactory?.CreateLogger() ?? 29 | new NullLogger(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Streams/ConditionalWriteStatus.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// The reason why a conditional write fails 4 | /// 5 | public enum ConditionalWriteStatus { 6 | /// 7 | /// The write operation succeeded 8 | /// 9 | Succeeded = 0, 10 | 11 | /// 12 | /// The expected version does not match actual stream version 13 | /// 14 | VersionMismatch = 1, 15 | 16 | /// 17 | /// The stream has been deleted 18 | /// 19 | StreamDeleted = 2 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Streams/DeadLine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KurrentDB.Client { 4 | #pragma warning disable CS1591 5 | public static class DeadLine { 6 | #pragma warning restore CS1591 7 | /// 8 | /// Represents no deadline (i.e., wait infinitely) 9 | /// 10 | public static TimeSpan? None = null; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Streams/DeleteResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KurrentDB.Client { 4 | /// 5 | /// A structure that represents the result of a delete operation. 6 | /// 7 | public readonly struct DeleteResult : IEquatable { 8 | /// 9 | public bool Equals(DeleteResult other) => LogPosition.Equals(other.LogPosition); 10 | 11 | /// 12 | public override bool Equals(object? obj) => obj is DeleteResult other && Equals(other); 13 | 14 | /// 15 | public override int GetHashCode() => LogPosition.GetHashCode(); 16 | 17 | /// 18 | /// Compares left and right for equality. 19 | /// 20 | /// 21 | /// 22 | /// True if left is equal to right. 23 | public static bool operator ==(DeleteResult left, DeleteResult right) => left.Equals(right); 24 | 25 | /// 26 | /// Compares left and right for inequality. 27 | /// 28 | /// 29 | /// 30 | /// True if left is not equal to right. 31 | public static bool operator !=(DeleteResult left, DeleteResult right) => !left.Equals(right); 32 | 33 | /// 34 | /// The of the delete in the transaction file. 35 | /// 36 | public readonly Position LogPosition; 37 | 38 | /// 39 | /// Constructs a new . 40 | /// 41 | /// 42 | public DeleteResult(Position logPosition) { 43 | LogPosition = logPosition; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Streams/Direction.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// An enumeration that indicates the direction of the read operation. 4 | /// 5 | public enum Direction { 6 | /// 7 | /// Read backwards. 8 | /// 9 | Backwards, 10 | 11 | /// 12 | /// Read forwards. 13 | /// 14 | Forwards 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Streams/IWriteResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KurrentDB.Client { 4 | /// 5 | /// An interface representing the result of a write operation. 6 | /// 7 | public interface IWriteResult { 8 | /// 9 | /// The version the stream is currently at. 10 | /// 11 | [Obsolete("Please use NextExpectedStreamRevision instead. This property will be removed in a future version.", 12 | true)] 13 | long NextExpectedVersion { get; } 14 | /// 15 | /// The of the in the transaction file. 16 | /// 17 | Position LogPosition { get; } 18 | /// 19 | /// The the stream is currently at. 20 | /// 21 | StreamState NextExpectedStreamState { get; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Streams/InvalidTransactionException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace KurrentDB.Client; 5 | 6 | /// 7 | /// Exception thrown if there is an attempt to operate inside a 8 | /// transaction which does not exist. 9 | /// 10 | public class InvalidTransactionException : Exception { 11 | /// 12 | /// Constructs a new . 13 | /// 14 | public InvalidTransactionException() { } 15 | 16 | /// 17 | /// Constructs a new . 18 | /// 19 | public InvalidTransactionException(string message) : base(message) { } 20 | 21 | /// 22 | /// Constructs a new . 23 | /// 24 | public InvalidTransactionException(string message, Exception innerException) : base(message, innerException) { } 25 | 26 | /// 27 | /// Constructs a new . 28 | /// 29 | [Obsolete("Obsolete")] 30 | protected InvalidTransactionException(SerializationInfo info, StreamingContext context) : base(info, context) { } 31 | } 32 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Streams/MaximumAppendSizeExceededException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KurrentDB.Client { 4 | /// 5 | /// Exception thrown when an append exceeds the maximum size set by the server. 6 | /// 7 | public class MaximumAppendSizeExceededException : Exception { 8 | /// 9 | /// The configured maximum append size. 10 | /// 11 | public uint MaxAppendSize { get; } 12 | 13 | /// 14 | /// Constructs a new . 15 | /// 16 | /// 17 | /// 18 | public MaximumAppendSizeExceededException(uint maxAppendSize, Exception? innerException = null) : 19 | base($"Maximum Append Size of {maxAppendSize} Exceeded.", innerException) { 20 | MaxAppendSize = maxAppendSize; 21 | } 22 | 23 | /// 24 | /// Constructs a new . 25 | /// 26 | /// 27 | /// 28 | public MaximumAppendSizeExceededException(int maxAppendSize, Exception? innerException = null) : this( 29 | (uint)maxAppendSize, innerException) { 30 | 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Streams/ReadState.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | /// An enumeration representing the state of a read operation. 4 | /// 5 | public enum ReadState { 6 | /// 7 | /// The stream does not exist. 8 | /// 9 | StreamNotFound, 10 | /// 11 | /// The stream exists. 12 | /// 13 | Ok 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Streams/Streams/AppendReq.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | 3 | namespace EventStore.Client.Streams { 4 | partial class AppendReq { 5 | public AppendReq WithAnyStreamRevision(StreamState expectedState) { 6 | if (expectedState == StreamState.Any) { 7 | Options.Any = new Empty(); 8 | } else if (expectedState == StreamState.NoStream) { 9 | Options.NoStream = new Empty(); 10 | } else if (expectedState == StreamState.StreamExists) { 11 | Options.StreamExists = new Empty(); 12 | } else { 13 | Options.Revision = (ulong)expectedState.ToInt64(); 14 | } 15 | 16 | return this; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Streams/Streams/BatchAppendReq.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf.WellKnownTypes; 2 | using KurrentDB.Client; 3 | 4 | namespace EventStore.Client.Streams { 5 | partial class BatchAppendReq { 6 | partial class Types { 7 | partial class Options { 8 | public static Options Create(StreamIdentifier streamIdentifier, StreamState expectedState, 9 | TimeSpan? timeoutAfter) { 10 | if (expectedState.HasPosition) { 11 | return new() { 12 | StreamIdentifier = streamIdentifier, 13 | StreamPosition = (ulong)expectedState.ToInt64(), 14 | Deadline21100 = Timestamp.FromDateTime( 15 | timeoutAfter.HasValue 16 | ? DateTime.UtcNow + timeoutAfter.Value 17 | : DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc) 18 | ) 19 | }; 20 | } 21 | 22 | return new Options { 23 | StreamIdentifier = streamIdentifier, 24 | expectedStreamPositionCase_ = expectedState switch { 25 | { } when expectedState == StreamState.Any => ExpectedStreamPositionOneofCase.Any, 26 | { } when expectedState == StreamState.NoStream => ExpectedStreamPositionOneofCase.NoStream, 27 | { } when expectedState == StreamState.StreamExists => ExpectedStreamPositionOneofCase 28 | .StreamExists, 29 | _ => ExpectedStreamPositionOneofCase.StreamPosition 30 | }, 31 | expectedStreamPosition_ = new Google.Protobuf.WellKnownTypes.Empty(), 32 | Deadline21100 = Timestamp.FromDateTime( 33 | timeoutAfter.HasValue 34 | ? DateTime.UtcNow + timeoutAfter.Value 35 | : DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc) 36 | ) 37 | }; 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Streams/Streams/DeleteReq.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | 3 | namespace EventStore.Client.Streams { 4 | partial class DeleteReq { 5 | public DeleteReq WithAnyStreamRevision(StreamState expectedState) { 6 | if (expectedState == StreamState.Any) { 7 | Options.Any = new Empty(); 8 | } else if (expectedState == StreamState.NoStream) { 9 | Options.NoStream = new Empty(); 10 | } else if (expectedState == StreamState.StreamExists) { 11 | Options.StreamExists = new Empty(); 12 | } else { 13 | Options.Revision = (ulong)expectedState.ToInt64(); 14 | } 15 | 16 | return this; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Streams/Streams/TombstoneReq.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | 3 | namespace EventStore.Client.Streams { 4 | partial class TombstoneReq { 5 | public TombstoneReq WithAnyStreamRevision(StreamState expectedState) { 6 | if (expectedState == StreamState.Any) { 7 | Options.Any = new Empty(); 8 | } else if (expectedState == StreamState.NoStream) { 9 | Options.NoStream = new Empty(); 10 | } else if (expectedState == StreamState.StreamExists) { 11 | Options.StreamExists = new Empty(); 12 | } else { 13 | Options.Revision = (ulong)expectedState.ToInt64(); 14 | } 15 | 16 | return this; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Streams/SystemEventTypes.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | /// 3 | ///Constants for System event types 4 | /// 5 | public static class SystemEventTypes { 6 | /// 7 | /// event type for stream deleted 8 | /// 9 | public const string StreamDeleted = "$streamDeleted"; 10 | 11 | /// 12 | /// event type for statistics 13 | /// 14 | public const string StatsCollection = "$statsCollected"; 15 | 16 | /// 17 | /// event type for linkTo 18 | /// 19 | public const string LinkTo = "$>"; 20 | 21 | /// 22 | /// event type for stream metadata 23 | /// 24 | public const string StreamMetadata = "$metadata"; 25 | 26 | /// 27 | /// event type for the system settings 28 | /// 29 | public const string Settings = "$settings"; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/Streams/WriteResultExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client { 2 | static class WriteResultExtensions { 3 | public static IWriteResult OptionallyThrowWrongExpectedVersionException( 4 | this IWriteResult writeResult, 5 | AppendToStreamOptions options 6 | ) => 7 | (options.ThrowOnAppendFailure, writeResult) switch { 8 | (true, WrongExpectedVersionResult wrongExpectedVersionResult) 9 | => throw new WrongExpectedVersionException( 10 | wrongExpectedVersionResult.StreamName, 11 | writeResult.NextExpectedStreamState, 12 | wrongExpectedVersionResult.ActualStreamState 13 | ), 14 | _ => writeResult 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/KurrentDB.Client/UserManagement/KurrentDBUserManagerClientExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client; 2 | 3 | /// 4 | /// A set of extension methods for an . 5 | /// 6 | public static class KurrentDBUserManagerClientExtensions { 7 | /// 8 | /// Gets the of the internal user specified by the supplied . 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// 15 | public static Task GetCurrentUserAsync( 16 | this KurrentDBUserManagementClient dbUsers, 17 | UserCredentials userCredentials, TimeSpan? deadline = null, CancellationToken cancellationToken = default 18 | ) => 19 | dbUsers.GetUserAsync( 20 | userCredentials.Username!, deadline, userCredentials, 21 | cancellationToken 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/.env: -------------------------------------------------------------------------------- 1 | ES_CERTS_CLUSTER=./certs-cluster 2 | ES_DOCKER_TAG=ci 3 | 4 | #EVENTSTORE_MEM_DB=true 5 | #EVENTSTORE_HTTP_PORT=2113 6 | #EVENTSTORE_LOG_LEVEL=Information 7 | #EVENTSTORE_DISABLE_LOG_FILE=true 8 | #EVENTSTORE_RUN_PROJECTIONS=None 9 | #EVENTSTORE_START_STANDARD_PROJECTIONS=true 10 | #EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH=/etc/eventstore/certs/ca 11 | #EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=false 12 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/Certificates.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | 3 | namespace KurrentDB.Client.Tests; 4 | 5 | public static class Certificates { 6 | static readonly string BaseDirectory = AppDomain.CurrentDomain.BaseDirectory; 7 | const string CertsFolder = "certs"; 8 | 9 | public static class TlsCa { 10 | public static string Absolute => GetAbsolutePath(CertsFolder, "ca", "ca.crt"); 11 | public static string Relative => GetRelativePath(CertsFolder, "ca", "ca.crt"); 12 | } 13 | 14 | public static class Admin { 15 | public static string CertAbsolute => GetAbsolutePath(CertsFolder, "user-admin", "user-admin.crt"); 16 | public static string CertRelative => GetRelativePath(CertsFolder, "user-admin", "user-admin.crt"); 17 | 18 | public static string KeyAbsolute => GetAbsolutePath(CertsFolder, "user-admin", "user-admin.key"); 19 | public static string KeyRelative => GetRelativePath(CertsFolder, "user-admin", "user-admin.key"); 20 | } 21 | 22 | public static class Invalid { 23 | public static string CertAbsolute => GetAbsolutePath(CertsFolder, "user-invalid", "user-invalid.crt"); 24 | public static string CertRelative => GetRelativePath(CertsFolder, "user-invalid", "user-invalid.crt"); 25 | 26 | public static string KeyAbsolute => GetAbsolutePath(CertsFolder, "user-invalid", "user-invalid.key"); 27 | public static string KeyRelative => GetRelativePath(CertsFolder, "user-invalid", "user-invalid.key"); 28 | } 29 | 30 | static string GetAbsolutePath(params string[] paths) => Path.Combine(BaseDirectory, Path.Combine(paths)); 31 | static string GetRelativePath(params string[] paths) => Path.Combine(paths); 32 | } 33 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/Extensions/ConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | 3 | namespace KurrentDB.Client.Tests; 4 | 5 | public static class ConfigurationExtensions { 6 | public static void EnsureValue(this IConfiguration configuration, string key, string defaultValue) { 7 | var value = configuration.GetValue(key); 8 | 9 | if (string.IsNullOrEmpty(value)) 10 | configuration[key] = defaultValue; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/Extensions/KurrentDBClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | using Polly; 3 | using static System.TimeSpan; 4 | 5 | namespace KurrentDB.Client.Tests; 6 | 7 | public static class KurrentDBClientExtensions { 8 | public static Task CreateUserWithRetry( 9 | this KurrentDBUserManagementClient client, string loginName, string fullName, string[] groups, string password, 10 | UserCredentials? userCredentials = null, CancellationToken cancellationToken = default 11 | ) => 12 | Policy.Handle() 13 | .WaitAndRetryAsync(200, _ => FromMilliseconds(100)) 14 | .ExecuteAsync( 15 | ct => client.CreateUserAsync( 16 | loginName, 17 | fullName, 18 | groups, 19 | password, 20 | userCredentials: userCredentials, 21 | cancellationToken: ct 22 | ), 23 | cancellationToken 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client; 2 | 3 | public static class OperatingSystemExtensions { 4 | public static bool IsWindows(this OperatingSystem operatingSystem) => 5 | operatingSystem.Platform != PlatformID.Unix 6 | && operatingSystem.Platform != PlatformID.MacOSX; 7 | } 8 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using KurrentDB.Client; 3 | 4 | namespace KurrentDB.Client; 5 | 6 | public static class ReadOnlyMemoryExtensions { 7 | public static Position ParsePosition(this ReadOnlyMemory json) { 8 | using var doc = JsonDocument.Parse(json); 9 | 10 | var checkPoint = doc.RootElement.GetString(); 11 | if (checkPoint is null) 12 | throw new("Unable to parse Position, data is missing!"); 13 | 14 | if (Position.TryParse(checkPoint, out var position) && position.HasValue) 15 | return position.Value; 16 | 17 | throw new("Unable to parse Position, invalid data!"); 18 | } 19 | 20 | public static StreamPosition ParseStreamPosition(this ReadOnlyMemory json) { 21 | using var doc = JsonDocument.Parse(json); 22 | 23 | var checkPoint = doc.RootElement.GetString(); 24 | if (checkPoint is null) 25 | throw new("Unable to parse Position, data is missing!"); 26 | 27 | return StreamPosition.FromInt64(int.Parse(checkPoint)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/Extensions/ShouldThrowAsyncExtensions.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | 3 | namespace KurrentDB.Client.Tests; 4 | 5 | public static class ShouldThrowAsyncExtensions { 6 | public static Task ShouldThrowAsync(this KurrentDBClient.ReadStreamResult source) where TException : Exception => 7 | source 8 | .ToArrayAsync() 9 | .AsTask() 10 | .ShouldThrowAsync(); 11 | 12 | public static async Task ShouldThrowAsync(this KurrentDBClient.ReadStreamResult source, Action handler) where TException : Exception { 13 | var ex = await source.ShouldThrowAsync(); 14 | handler(ex); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/Extensions/TaskExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace KurrentDB.Client; 4 | 5 | public static class TaskExtensions { 6 | public static Task WithTimeout(this Task task, TimeSpan timeout) 7 | => task.WithTimeout(Convert.ToInt32(timeout.TotalMilliseconds)); 8 | 9 | public static async Task WithTimeout(this Task task, int timeoutMs = 15000, string? message = null) { 10 | if (Debugger.IsAttached) timeoutMs = -1; 11 | 12 | if (await Task.WhenAny(task, Task.Delay(timeoutMs)) != task) 13 | throw new TimeoutException(message ?? "Timed out waiting for task"); 14 | 15 | await task; 16 | } 17 | 18 | public static Task WithTimeout(this Task task, TimeSpan timeout) 19 | => task.WithTimeout(Convert.ToInt32(timeout.TotalMilliseconds)); 20 | 21 | public static async Task WithTimeout(this Task task, int timeoutMs = 15000, string? message = null) { 22 | if (Debugger.IsAttached) timeoutMs = -1; 23 | 24 | if (await Task.WhenAny(task, Task.Delay(timeoutMs)) == task) 25 | return await task; 26 | 27 | throw new TimeoutException(message ?? "Timed out waiting for task"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/Facts/AnonymousAccess.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client.Tests; 2 | 3 | [PublicAPI] 4 | public class AnonymousAccess { 5 | static readonly Version LegacySince = new(23, 6); 6 | static readonly string SkipMessage = "Anonymous access is turned off since v23.6.0!"; 7 | 8 | public class FactAttribute() : Deprecation.FactAttribute(LegacySince, SkipMessage); 9 | 10 | public class TheoryAttribute() : Deprecation.TheoryAttribute(LegacySince, SkipMessage); 11 | } 12 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/Facts/Deprecation.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client.Tests; 2 | 3 | [PublicAPI] 4 | public class Deprecation { 5 | public class FactAttribute(Version since, string skipMessage) : Xunit.FactAttribute { 6 | public override string? Skip { 7 | get => KurrentDBPermanentTestNode.Version >= since ? skipMessage : null; 8 | set => throw new NotSupportedException(); 9 | } 10 | } 11 | 12 | public class TheoryAttribute : Xunit.TheoryAttribute { 13 | readonly Version _legacySince; 14 | readonly string _skipMessage; 15 | 16 | public TheoryAttribute(Version since, string skipMessage) { 17 | _legacySince = since; 18 | _skipMessage = skipMessage; 19 | } 20 | 21 | public override string? Skip { 22 | get => KurrentDBPermanentTestNode.Version >= _legacySince ? _skipMessage : null; 23 | set => throw new NotSupportedException(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/Facts/Regression.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client.Tests; 2 | 3 | [PublicAPI] 4 | public class Regression { 5 | public class FactAttribute(int major, string skipMessage) : Xunit.FactAttribute { 6 | public override string? Skip { 7 | get => (KurrentDBPermanentTestNode.Version?.Major ?? int.MaxValue) < major ? skipMessage : null; 8 | set => throw new NotSupportedException(); 9 | } 10 | } 11 | 12 | public class TheoryAttribute(int major, string skipMessage) : Xunit.TheoryAttribute { 13 | public override string? Skip { 14 | get => (KurrentDBPermanentTestNode.Version?.Major ?? int.MaxValue) < major ? skipMessage : null; 15 | set => throw new NotSupportedException(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/Fixtures/KurrentDBFixtureOptions.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client.Tests; 2 | 3 | public record KurrentDBFixtureOptions( 4 | KurrentDBClientSettings DbClientSettings, 5 | IDictionary Environment 6 | ) { 7 | public KurrentDBFixtureOptions RunInMemory(bool runInMemory = true) => 8 | this with { Environment = Environment.With(x => x["EVENTSTORE_MEM_DB"] = runInMemory.ToString()) }; 9 | 10 | public KurrentDBFixtureOptions WithoutDefaultCredentials() => this with { DbClientSettings = DbClientSettings.With(x => x.DefaultCredentials = null) }; 11 | 12 | public KurrentDBFixtureOptions RunProjections(bool runProjections = true) => 13 | this with { 14 | Environment = Environment.With( 15 | x => { 16 | x["EVENTSTORE_START_STANDARD_PROJECTIONS"] = runProjections.ToString(); 17 | x["EVENTSTORE_RUN_PROJECTIONS"] = runProjections ? "All" : "None"; 18 | } 19 | ) 20 | }; 21 | } 22 | 23 | public delegate KurrentDBFixtureOptions ConfigureFixture(KurrentDBFixtureOptions options); 24 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/FluentDocker/TestCompositeService.cs: -------------------------------------------------------------------------------- 1 | using Ductus.FluentDocker.Builders; 2 | using Ductus.FluentDocker.Services; 3 | 4 | namespace KurrentDB.Client.Tests.FluentDocker; 5 | 6 | public abstract class TestCompositeService : TestService; 7 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/FluentDocker/TestContainerService.cs: -------------------------------------------------------------------------------- 1 | using Ductus.FluentDocker.Builders; 2 | using Ductus.FluentDocker.Services; 3 | 4 | namespace KurrentDB.Client.Tests.FluentDocker; 5 | 6 | public abstract class TestContainerService : TestService; 7 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable CheckNamespace 2 | 3 | using System.Diagnostics; 4 | using KurrentDB.Client; 5 | 6 | namespace Shouldly; 7 | 8 | [DebuggerStepThrough] 9 | public static class ShouldThrowAsyncExtensions { 10 | public static Task ShouldThrowAsync(this KurrentDBClient.ReadStreamResult source) where TException : Exception => 11 | source.ToArrayAsync().AsTask().ShouldThrowAsync(); 12 | } 13 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/TestCaseGenerator.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client.Tests; 2 | using System.Collections; 3 | using Bogus; 4 | 5 | public abstract class TestCaseGenerator : ClassDataAttribute, IEnumerable { 6 | protected TestCaseGenerator() : base(typeof(T)) { 7 | Faker = new Faker(); 8 | 9 | // ReSharper disable once VirtualMemberCallInConstructor 10 | Generated.AddRange(Data()); 11 | 12 | if (Generated.Count == 0) 13 | throw new InvalidOperationException($"TestDataGenerator<{typeof(T).Name}> must provide at least one test case."); 14 | } 15 | 16 | protected Faker Faker { get; } 17 | 18 | List Generated { get; } = []; 19 | 20 | public IEnumerator GetEnumerator() => Generated.GetEnumerator(); 21 | 22 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 23 | 24 | protected abstract IEnumerable Data(); 25 | } 26 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/TestCredentials.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client.Tests; 2 | 3 | public static class TestCredentials { 4 | public static readonly UserCredentials Root = new("admin", "changeit"); 5 | public static readonly UserCredentials TestUser1 = new("user1", "pa$$1"); 6 | public static readonly UserCredentials TestUser2 = new("user2", "pa$$2"); 7 | public static readonly UserCredentials TestAdmin = new("adm", "admpa$$"); 8 | public static readonly UserCredentials TestBadUser = new("badlogin", "badpass"); 9 | } 10 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "MinimumLevel": { 4 | "Default": "Debug", 5 | "Override": { 6 | "Microsoft": "Warning", 7 | "Grpc": "Information", 8 | "Grpc.Net.Client.Internal.GrpcCall": "Fatal", 9 | "EventStore.Client.SharingProvider": "Information", 10 | "EventStore.Client.EventStoreClient": "Information", 11 | "EventStore.Client.SingleNodeChannelSelector": "Warning" 12 | } 13 | }, 14 | "Enrich": ["FromLogContext", "WithThreadId"], 15 | "WriteTo": [ 16 | { 17 | "Name": "Console", 18 | "Args": { 19 | "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Literate, Serilog.Sinks.Console", 20 | "outputTemplate": "[{Timestamp:mm:ss.fff} {Level:u3}] {TestRunId} ({ThreadId:000}) {SourceContext} {Message}{NewLine}{Exception}" 21 | } 22 | }, 23 | { 24 | "Name": "Seq", 25 | "Args": { 26 | "serverUrl": "http://localhost:5341" 27 | } 28 | } 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "MinimumLevel": { 4 | "Default": "Debug", 5 | "Override": { 6 | "Microsoft": "Warning", 7 | "Grpc": "Information", 8 | "Grpc.Net.Client.Internal.GrpcCall": "Fatal", 9 | "EventStore.Client.SharingProvider": "Information", 10 | "EventStore.Client.EventStoreClient": "Information", 11 | "EventStore.Client.SingleNodeChannelSelector": "Warning" 12 | } 13 | }, 14 | "Enrich": ["FromLogContext", "WithThreadId"], 15 | "WriteTo": [ 16 | { 17 | "Name": "Console", 18 | "Args": { 19 | "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Literate, Serilog.Sinks.Console", 20 | "outputTemplate": "[{Timestamp:mm:ss.fff} {Level:u3}] {TestRunId} ({ThreadId:000}) {SourceContext} {Message}{NewLine}{Exception}" 21 | } 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/docker-compose.certs.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | networks: 4 | default: 5 | name: eventstore-network 6 | 7 | services: 8 | 9 | volumes-provisioner: 10 | image: hasnat/volumes-provisioner 11 | container_name: volumes-provisioner 12 | environment: 13 | PROVISION_DIRECTORIES: "1000:1000:0755:/tmp/certs" 14 | volumes: 15 | - "${ES_CERTS_CLUSTER}:/tmp/certs" 16 | network_mode: none 17 | 18 | cert-gen: 19 | image: docker.eventstore.com/eventstore-utils/es-gencert-cli:latest 20 | container_name: cert-gen 21 | user: "1000:1000" 22 | entrypoint: [ "/bin/sh","-c" ] 23 | command: 24 | - | 25 | es-gencert-cli create-ca -out /tmp/certs/ca 26 | es-gencert-cli create-node -ca-certificate /tmp/certs/ca/ca.crt -ca-key /tmp/certs/ca/ca.key -out /tmp/certs/node -ip-addresses 127.0.0.1 -dns-names localhost,eventstore 27 | 28 | es-gencert-cli create-node -ca-certificate /tmp/certs/ca/ca.crt -ca-key /tmp/certs/ca/ca.key -out /tmp/certs/node1 -ip-addresses 127.0.0.1,172.30.240.11 -dns-names localhost,esdb-node1 29 | es-gencert-cli create-node -ca-certificate /tmp/certs/ca/ca.crt -ca-key /tmp/certs/ca/ca.key -out /tmp/certs/node2 -ip-addresses 127.0.0.1,172.30.240.12 -dns-names localhost,esdb-node2 30 | es-gencert-cli create-node -ca-certificate /tmp/certs/ca/ca.crt -ca-key /tmp/certs/ca/ca.key -out /tmp/certs/node3 -ip-addresses 127.0.0.1,172.30.240.13 -dns-names localhost,esdb-node3 31 | es-gencert-cli create-node -ca-certificate /tmp/certs/ca/ca.crt -ca-key /tmp/certs/ca/ca.key -out /tmp/certs/node4 -ip-addresses 127.0.0.1,172.30.240.14 -dns-names localhost,esdb-node4 32 | volumes: 33 | - "${ES_CERTS_CLUSTER}:/tmp/certs" 34 | depends_on: 35 | - volumes-provisioner 36 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/docker-compose.node.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | networks: 4 | default: 5 | name: eventstore-network 6 | 7 | services: 8 | 9 | eventstore: 10 | image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:${ES_DOCKER_TAG} 11 | container_name: eventstore 12 | environment: 13 | - EVENTSTORE_MEM_DB=true 14 | - EVENTSTORE_HTTP_PORT=2113 15 | - EVENTSTORE_LOG_LEVEL=Information 16 | - EVENTSTORE_RUN_PROJECTIONS=None 17 | - EVENTSTORE_START_STANDARD_PROJECTIONS=true 18 | 19 | # set certificates location 20 | - EVENTSTORE_CERTIFICATE_FILE=/etc/eventstore/certs/node/node.crt 21 | - EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE=/etc/eventstore/certs/node/node.key 22 | - EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH=/etc/eventstore/certs/ca 23 | ports: 24 | - "2113:2113" 25 | volumes: 26 | - ${ES_CERTS_CLUSTER}:/etc/eventstore/certs 27 | - type: volume 28 | source: eventstore-volume-data1 29 | target: /var/lib/eventstore 30 | - type: volume 31 | source: eventstore-volume-logs1 32 | target: /var/log/eventstore 33 | restart: unless-stopped 34 | 35 | volumes: 36 | eventstore-volume-data1: 37 | eventstore-volume-logs1: 38 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.Common/shared.env: -------------------------------------------------------------------------------- 1 | EVENTSTORE_CLUSTER_SIZE=3 2 | EVENTSTORE_INT_TCP_PORT=1112 3 | EVENTSTORE_HTTP_PORT=2113 4 | EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH=/etc/eventstore/certs/ca 5 | EVENTSTORE_DISCOVER_VIA_DNS=false 6 | EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=false 7 | EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE=10000 8 | 9 | # pass through from environment 10 | EVENTSTORE_DB_LOG_FORMAT 11 | EVENTSTORE_LOG_LEVEL 12 | EVENTSTORE_MAX_APPEND_SIZE 13 | EVENTSTORE_MEM_DB 14 | EVENTSTORE_RUN_PROJECTIONS 15 | EVENTSTORE_START_STANDARD_PROJECTIONS 16 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.ExternalAssembly/ExternalEvents.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client.Tests.ExternalAssembly; 2 | 3 | /// 4 | /// External event class used for testing loaded assembly resolution 5 | /// This assembly will be explicitly loaded during tests 6 | /// 7 | public class ExternalEvent { 8 | public string Id { get; set; } = null!; 9 | public string Name { get; set; } = null!; 10 | } 11 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.ExternalAssembly/KurrentDB.Client.Tests.ExternalAssembly.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | KurrentDB.Client.Tests.ExternalAssembly 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.NeverLoadedAssembly/KurrentDB.Client.Tests.NeverLoadedAssembly.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | KurrentDB.Client.Tests.NeverLoadedAssembly 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests.NeverLoadedAssembly/NotLoadedExternalEvent.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client.Tests.NeverLoadedAssembly; 2 | 3 | /// 4 | /// External event class used for testing unloaded assembly resolution 5 | /// This event should never be referenced directly by the test project 6 | /// 7 | public record NotLoadedExternalEvent { 8 | public string Id { get; set; } = null!; 9 | public string Name { get; set; } = null!; 10 | } 11 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Assertions/NullArgumentAssertion.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using AutoFixture.Idioms; 3 | using AutoFixture.Kernel; 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace KurrentDB.Client; 7 | 8 | class NullArgumentAssertion : IdiomaticAssertion { 9 | readonly ISpecimenBuilder _builder; 10 | 11 | public NullArgumentAssertion(ISpecimenBuilder builder) => _builder = builder; 12 | 13 | public override void Verify(Type type) { 14 | var context = new SpecimenContext(_builder); 15 | 16 | Assert.All( 17 | type.GetConstructors(), 18 | constructor => { 19 | var parameters = constructor.GetParameters(); 20 | 21 | Assert.All( 22 | parameters.Where( 23 | p => p.ParameterType.IsClass || 24 | p.ParameterType == typeof(string) || 25 | (p.ParameterType.IsGenericType && 26 | p.ParameterType.GetGenericArguments().FirstOrDefault() == 27 | typeof(Nullable<>)) 28 | ), 29 | p => { 30 | var args = new object[parameters.Length]; 31 | 32 | for (var i = 0; i < args.Length; i++) 33 | if (i != p.Position) 34 | args[i] = context.Resolve(p.ParameterType); 35 | 36 | var ex = Assert.Throws( 37 | () => { 38 | try { 39 | constructor.Invoke(args); 40 | } 41 | catch (TargetInvocationException ex) { 42 | throw ex.InnerException!; 43 | } 44 | } 45 | ); 46 | 47 | Assert.Equal(p.Name, ex.ParamName); 48 | } 49 | ); 50 | } 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Assertions/StringConversionAssertion.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using AutoFixture.Idioms; 3 | using AutoFixture.Kernel; 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace KurrentDB.Client; 7 | 8 | class StringConversionAssertion : IdiomaticAssertion { 9 | readonly ISpecimenBuilder _builder; 10 | 11 | public StringConversionAssertion(ISpecimenBuilder builder) => _builder = builder; 12 | 13 | public override void Verify(Type type) { 14 | var context = new SpecimenContext(_builder); 15 | 16 | var constructor = type.GetConstructor(new[] { typeof(string) }); 17 | 18 | if (constructor is null) 19 | return; 20 | 21 | var value = (string)context.Resolve(typeof(string)); 22 | var instance = constructor.Invoke(new object[] { value }); 23 | var args = new[] { instance }; 24 | 25 | var @explicit = type 26 | .GetMethods(BindingFlags.Public | BindingFlags.Static) 27 | .FirstOrDefault(m => m.Name == "op_Explicit" && m.ReturnType == typeof(string)); 28 | 29 | if (@explicit is not null) 30 | Assert.Equal(value, @explicit.Invoke(null, args)); 31 | 32 | var @implicit = type 33 | .GetMethods(BindingFlags.Public | BindingFlags.Static) 34 | .FirstOrDefault(m => m.Name == "op_Implicit" && m.ReturnType == typeof(string)); 35 | 36 | if (@implicit is not null) 37 | Assert.Equal(value, @implicit.Invoke(null, args)); 38 | 39 | var toString = type 40 | .GetMethods(BindingFlags.Public | BindingFlags.Public) 41 | .FirstOrDefault(m => m.Name == "ToString" && m.ReturnType == typeof(string)); 42 | 43 | if (toString is not null) 44 | Assert.Equal(value, toString.Invoke(instance, null)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Assertions/ValueObjectAssertion.cs: -------------------------------------------------------------------------------- 1 | using AutoFixture.Idioms; 2 | using AutoFixture.Kernel; 3 | 4 | // ReSharper disable once CheckNamespace 5 | namespace KurrentDB.Client; 6 | 7 | class ValueObjectAssertion : CompositeIdiomaticAssertion { 8 | public ValueObjectAssertion(ISpecimenBuilder builder) : base(CreateChildrenAssertions(builder)) { } 9 | 10 | static IEnumerable CreateChildrenAssertions(ISpecimenBuilder builder) { 11 | yield return new EqualityAssertion(builder); 12 | yield return new ComparableAssertion(builder); 13 | yield return new StringConversionAssertion(builder); 14 | yield return new NullArgumentAssertion(builder); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/AutoScenarioDataAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using AutoFixture; 3 | using AutoFixture.Xunit2; 4 | using Xunit.Sdk; 5 | 6 | namespace KurrentDB.Client.Tests; 7 | 8 | [DataDiscoverer("AutoFixture.Xunit2.NoPreDiscoveryDataDiscoverer", "AutoFixture.Xunit2")] 9 | public class AutoScenarioDataAttribute : DataAttribute { 10 | readonly Type _fixtureType; 11 | 12 | public AutoScenarioDataAttribute(Type fixtureType, int iterations = 3) { 13 | _fixtureType = fixtureType; 14 | Iterations = iterations; 15 | } 16 | 17 | public int Iterations { get; } 18 | 19 | public override IEnumerable GetData(MethodInfo testMethod) { 20 | var customAutoData = new CustomAutoData(_fixtureType); 21 | 22 | return Enumerable.Range(0, Iterations).SelectMany(_ => customAutoData.GetData(testMethod)); 23 | } 24 | 25 | class CustomAutoData : AutoDataAttribute { 26 | public CustomAutoData(Type fixtureType) : base(() => (IFixture)Activator.CreateInstance(fixtureType)!) { } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Core/Serialization/MessageTypeNamingResolutionContextTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Core.Serialization; 2 | 3 | namespace KurrentDB.Client.Tests.Core.Serialization; 4 | 5 | public class MessageTypeNamingResolutionContextTests 6 | { 7 | [Fact] 8 | public void CategoryName_ExtractsFromStreamName() 9 | { 10 | // Arrange 11 | var context = MessageTypeNamingResolutionContext.FromStreamName("user-123"); 12 | 13 | // Act 14 | var categoryName = context.CategoryName; 15 | 16 | // Assert 17 | Assert.Equal("user", categoryName); 18 | } 19 | 20 | [Fact] 21 | public void CategoryName_ExtractsFromStreamNameWithMoreThanOneDash() 22 | { 23 | // Arrange 24 | var context = MessageTypeNamingResolutionContext.FromStreamName("user-some-123"); 25 | 26 | // Act 27 | var categoryName = context.CategoryName; 28 | 29 | // Assert 30 | Assert.Equal("user", categoryName); 31 | } 32 | 33 | [Fact] 34 | public void CategoryName_ReturnsTheWholeStreamName() 35 | { 36 | // Arrange 37 | var context = MessageTypeNamingResolutionContext.FromStreamName("user123"); 38 | 39 | // Act 40 | var categoryName = context.CategoryName; 41 | 42 | // Assert 43 | Assert.Equal("user123", categoryName); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Core/Serialization/NullMessageSerializerTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Core.Serialization; 2 | 3 | namespace KurrentDB.Client.Tests.Core.Serialization; 4 | 5 | using static MessageTypeNamingResolutionContext; 6 | 7 | public class NullMessageSerializerTests { 8 | [Fact] 9 | public void Serialize_ThrowsException() { 10 | // Given 11 | var serializer = NullMessageSerializer.Instance; 12 | var message = Message.From(new object()); 13 | var context = new MessageSerializationContext(FromStreamName("test-stream")); 14 | 15 | // When & Assert 16 | Assert.Throws(() => serializer.Serialize(message, context)); 17 | } 18 | 19 | [Fact] 20 | public void TryDeserialize_ReturnsFalse() { 21 | // Given 22 | var serializer = NullMessageSerializer.Instance; 23 | var eventRecord = CreateTestEventRecord(); 24 | 25 | // When 26 | var result = serializer.TryDeserialize(eventRecord, out var message); 27 | 28 | // Then 29 | Assert.False(result); 30 | Assert.Null(message); 31 | } 32 | 33 | static EventRecord CreateTestEventRecord() => 34 | new( 35 | Uuid.NewUuid().ToString(), 36 | Uuid.NewUuid(), 37 | StreamPosition.FromInt64(0), 38 | new Position(1, 1), 39 | new Dictionary { 40 | { Constants.Metadata.Type, "test-event" }, 41 | { Constants.Metadata.Created, DateTime.UtcNow.ToTicksSinceEpoch().ToString() }, 42 | { Constants.Metadata.ContentType, Constants.Metadata.ContentTypes.ApplicationJson } 43 | }, 44 | """{"x":1}"""u8.ToArray(), 45 | """{"x":2}"""u8.ToArray() 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/InvalidCredentialsTestCases.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using KurrentDB.Client; 3 | 4 | namespace KurrentDB.Client.Tests; 5 | 6 | public abstract record InvalidCredentialsTestCase(TestUser User, Type ExpectedException); 7 | 8 | public class InvalidCredentialsTestCases : IEnumerable { 9 | public IEnumerator GetEnumerator() { 10 | yield return new object?[] { new MissingCredentials() }; 11 | yield return new object?[] { new WrongUsername() }; 12 | yield return new object?[] { new WrongPassword() }; 13 | } 14 | 15 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 16 | 17 | public record MissingCredentials() : InvalidCredentialsTestCase(Fakers.Users.WithNoCredentials(), typeof(AccessDeniedException)) { 18 | public override string ToString() => nameof(MissingCredentials); 19 | } 20 | 21 | public record WrongUsername() : InvalidCredentialsTestCase(Fakers.Users.WithInvalidCredentials(false), typeof(NotAuthenticatedException)) { 22 | public override string ToString() => nameof(WrongUsername); 23 | } 24 | 25 | public record WrongPassword() : InvalidCredentialsTestCase(Fakers.Users.WithInvalidCredentials(wrongPassword: false), typeof(NotAuthenticatedException)) { 26 | public override string ToString() => nameof(WrongPassword); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/KurrentDB.Client.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/KurrentDBClientOperationsTests.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client.Tests; 2 | 3 | [Trait("Category", "Target:Misc")] 4 | public class KurrentDBClientOperationOptionsTests { 5 | [RetryFact] 6 | public void setting_options_on_clone_should_not_modify_original() { 7 | var options = KurrentDBClientOperationOptions.Default; 8 | 9 | var clonedOptions = options.Clone(); 10 | clonedOptions.BatchAppendSize = int.MaxValue; 11 | 12 | Assert.Equal(options.BatchAppendSize, KurrentDBClientOperationOptions.Default.BatchAppendSize); 13 | Assert.Equal(int.MaxValue, clonedOptions.BatchAppendSize); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Operations/MergeIndexTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | 3 | namespace KurrentDB.Client.Tests; 4 | 5 | [Trait("Category", "Target:DbOperations")] 6 | public class MergeIndexTests(ITestOutputHelper output, MergeIndexTests.CustomFixture fixture) 7 | : KurrentPermanentTests(output, fixture) { 8 | [RetryFact] 9 | public async Task merge_indexes_does_not_throw() => 10 | await Fixture.DbOperations 11 | .MergeIndexesAsync(userCredentials: TestCredentials.Root) 12 | .ShouldNotThrowAsync(); 13 | 14 | [RetryFact] 15 | public async Task merge_indexes_without_credentials_throws() => 16 | await Fixture.DbOperations 17 | .MergeIndexesAsync() 18 | .ShouldThrowAsync(); 19 | 20 | public class CustomFixture() : KurrentDBPermanentFixture(x => x.WithoutDefaultCredentials()); 21 | } 22 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Operations/ResignNodeTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | using KurrentDB.Client; 3 | 4 | namespace KurrentDB.Client.Tests.Operations; 5 | 6 | [Trait("Category", "Target:DbOperations")] 7 | public class ResignNodeTests(ITestOutputHelper output, ResignNodeTests.CustomFixture fixture) 8 | : KurrentTemporaryTests(output, fixture) { 9 | [RetryFact] 10 | public async Task resign_node_does_not_throw() => 11 | await Fixture.DbOperations 12 | .ResignNodeAsync(userCredentials: TestCredentials.Root) 13 | .ShouldNotThrowAsync(); 14 | 15 | [RetryFact] 16 | public async Task resign_node_without_credentials_throws() => 17 | await Fixture.DbOperations 18 | .ResignNodeAsync() 19 | .ShouldThrowAsync(); 20 | 21 | public class CustomFixture() : KurrentDBTemporaryFixture(x => x.WithoutDefaultCredentials()); 22 | } 23 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | using KurrentDB.Client; 3 | 4 | namespace KurrentDB.Client.Tests.Operations; 5 | 6 | [Trait("Category", "Target:DbOperations")] 7 | public class RestartPersistentSubscriptionsTests(ITestOutputHelper output, RestartPersistentSubscriptionsTests.CustomFixture fixture) 8 | : KurrentTemporaryTests(output, fixture) { 9 | [RetryFact] 10 | public async Task restart_persistent_subscriptions_does_not_throw() => 11 | await Fixture.DbOperations 12 | .RestartPersistentSubscriptions(userCredentials: TestCredentials.Root) 13 | .ShouldNotThrowAsync(); 14 | 15 | [RetryFact] 16 | public async Task restart_persistent_subscriptions_without_credentials_throws() => 17 | await Fixture.DbOperations 18 | .RestartPersistentSubscriptions() 19 | .ShouldThrowAsync(); 20 | 21 | public class CustomFixture() : KurrentDBTemporaryFixture(x => x.WithoutDefaultCredentials()); 22 | } 23 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | using KurrentDB.Client; 3 | 4 | namespace KurrentDB.Client.Tests; 5 | 6 | [Trait("Category", "Target:DbOperations")] 7 | public class ShutdownNodeAuthenticationTests(ITestOutputHelper output, ShutdownNodeAuthenticationTests.CustomFixture fixture) 8 | : KurrentTemporaryTests(output, fixture) { 9 | [RetryFact] 10 | public async Task shutdown_without_credentials_throws() => 11 | await Fixture.DbOperations.ShutdownAsync().ShouldThrowAsync(); 12 | 13 | public class CustomFixture() : KurrentDBTemporaryFixture(x => x.WithoutDefaultCredentials()); 14 | } 15 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Operations/ShutdownNodeTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | using KurrentDB.Client.Tests; 3 | 4 | namespace KurrentDB.Client.Tests.Operations; 5 | 6 | [Trait("Category", "Target:DbOperations")] 7 | public class ShutdownNodeTests(ITestOutputHelper output, ShutdownNodeTests.NoDefaultCredentialsFixture fixture) 8 | : KurrentTemporaryTests(output, fixture) { 9 | [RetryFact] 10 | public async Task shutdown_does_not_throw() => 11 | await Fixture.DbOperations.ShutdownAsync(userCredentials: TestCredentials.Root).ShouldNotThrowAsync(); 12 | 13 | public class NoDefaultCredentialsFixture() : KurrentDBTemporaryFixture(x => x.WithoutDefaultCredentials()); 14 | } 15 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/PersistentSubscriptions/FilterTestCases.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using KurrentDB.Client; 3 | 4 | namespace KurrentDB.Client.Tests.PersistentSubscriptions; 5 | 6 | public static class Filters { 7 | const string StreamNamePrefix = nameof(StreamNamePrefix); 8 | const string StreamNameRegex = nameof(StreamNameRegex); 9 | const string EventTypePrefix = nameof(EventTypePrefix); 10 | const string EventTypeRegex = nameof(EventTypeRegex); 11 | 12 | static readonly IDictionary, Func)> 13 | s_filters = 14 | new Dictionary, Func)> { 15 | [StreamNamePrefix] = (StreamFilter.Prefix, (_, e) => e), 16 | [StreamNameRegex] = (f => StreamFilter.RegularExpression(f), (_, e) => e), 17 | [EventTypePrefix] = (EventTypeFilter.Prefix, (term, e) => new( 18 | term, 19 | e.Data, 20 | e.Metadata, 21 | e.MessageId, 22 | e.ContentType 23 | )), 24 | [EventTypeRegex] = (f => EventTypeFilter.RegularExpression(f), (term, e) => new( 25 | term, 26 | e.Data, 27 | e.Metadata, 28 | e.MessageId, 29 | e.ContentType 30 | )) 31 | }; 32 | 33 | public static readonly IEnumerable All = typeof(Filters) 34 | .GetFields(BindingFlags.NonPublic | BindingFlags.Static) 35 | .Where(fi => fi.IsLiteral && !fi.IsInitOnly) 36 | .Select(fi => (string)fi.GetRawConstantValue()!); 37 | 38 | public static (Func getFilter, Func prepareEvent) 39 | GetFilter(string name) => 40 | s_filters[name]; 41 | } 42 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectWithoutReadPermissionsObsoleteTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | using KurrentDB.Client; 3 | 4 | namespace KurrentDB.Client.Tests.PersistentSubscriptions; 5 | 6 | [Trait("Category", "Target:PersistentSubscriptions")] 7 | public class SubscribeToAllConnectWithoutReadPermissionsObsoleteTests(ITestOutputHelper output, KurrentDBTemporaryFixture fixture) 8 | : KurrentTemporaryTests(output, fixture) { 9 | [RetryFact] 10 | public async Task connect_to_existing_without_read_all_permissions() { 11 | var group = Fixture.GetGroupName(); 12 | var user = Fixture.GetUserCredentials(); 13 | 14 | await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); 15 | 16 | await Fixture.DbUsers.CreateUserWithRetry( 17 | user.Username!, 18 | user.Username!, 19 | [], 20 | user.Password!, 21 | TestCredentials.Root 22 | ); 23 | 24 | await Assert.ThrowsAsync( 25 | async () => { 26 | using var _ = await Fixture.Subscriptions.SubscribeToAllAsync( 27 | group, 28 | delegate { return Task.CompletedTask; }, 29 | new SubscribeToPersistentSubscriptionOptions { UserCredentials = user } 30 | ); 31 | } 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllResultWithNormalUserCredentialsObsoleteTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | using KurrentDB.Client.Tests; 3 | 4 | namespace KurrentDB.Client.Tests.PersistentSubscriptions; 5 | 6 | [Trait("Category", "Target:PersistentSubscriptions")] 7 | public class SubscribeToAllResultWithNormalUserCredentialsObsoleteTests(ITestOutputHelper output, KurrentDBTemporaryFixture fixture) 8 | : KurrentTemporaryTests(output, fixture) { 9 | [RetryFact] 10 | public async Task returns_result_with_normal_user_credentials() { 11 | var group = Fixture.GetGroupName(); 12 | var stream = Fixture.GetStreamName(); 13 | 14 | const int streamSubscriptionCount = 4; 15 | const int allStreamSubscriptionCount = 3; 16 | 17 | for (var i = 0; i < streamSubscriptionCount; i++) 18 | await Fixture.Subscriptions.CreateToStreamAsync( 19 | stream, 20 | group + i, 21 | new(), 22 | userCredentials: TestCredentials.Root 23 | ); 24 | 25 | for (var i = 0; i < allStreamSubscriptionCount; i++) 26 | await Fixture.Subscriptions.CreateToAllAsync( 27 | group + i, 28 | new(), 29 | userCredentials: TestCredentials.Root 30 | ); 31 | 32 | var result = await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root); 33 | Assert.Equal(allStreamSubscriptionCount, result.Count()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReturnsAllSubscriptionsObsoleteTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | using KurrentDB.Client.Tests; 3 | 4 | namespace KurrentDB.Client.Tests.PersistentSubscriptions; 5 | 6 | [Trait("Category", "Target:PersistentSubscriptions")] 7 | public class SubscribeToAllReturnsAllSubscriptionsObsoleteTests(ITestOutputHelper output, SubscribeToAllReturnsAllSubscriptions.CustomFixture fixture) 8 | : KurrentTemporaryTests(output, fixture) { 9 | [RetryFact] 10 | public async Task returns_all_subscriptions() { 11 | var group = Fixture.GetGroupName(); 12 | var stream = Fixture.GetStreamName(); 13 | 14 | const int streamSubscriptionCount = 4; 15 | const int allStreamSubscriptionCount = 3; 16 | const int totalSubscriptionCount = streamSubscriptionCount + allStreamSubscriptionCount; 17 | 18 | for (var i = 0; i < streamSubscriptionCount; i++) 19 | await Fixture.Subscriptions.CreateToStreamAsync( 20 | stream, 21 | group + i, 22 | new(), 23 | userCredentials: TestCredentials.Root 24 | ); 25 | 26 | for (var i = 0; i < allStreamSubscriptionCount; i++) 27 | await Fixture.Subscriptions.CreateToAllAsync( 28 | group + i, 29 | new(), 30 | userCredentials: TestCredentials.Root 31 | ); 32 | 33 | var result = (await Fixture.Subscriptions.ListAllAsync(userCredentials: TestCredentials.Root)).ToList(); 34 | Assert.Equal(totalSubscriptionCount, result.Count); 35 | } 36 | 37 | public class CustomFixture : KurrentDBTemporaryFixture { 38 | public CustomFixture() { 39 | SkipPsWarmUp = true; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReturnsSubscriptionsToAllStreamObsoleteTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | using KurrentDB.Client.Tests; 3 | 4 | namespace KurrentDB.Client.Tests.PersistentSubscriptions; 5 | 6 | [Trait("Category", "Target:PersistentSubscriptions")] 7 | public class SubscribeToAllReturnsSubscriptionsToAllStreamObsoleteTests(ITestOutputHelper output, KurrentDBTemporaryFixture fixture) 8 | : KurrentTemporaryTests(output, fixture) { 9 | [RetryFact] 10 | public async Task returns_subscriptions_to_all_stream() { 11 | var group = Fixture.GetGroupName(); 12 | var stream = Fixture.GetStreamName(); 13 | 14 | const int streamSubscriptionCount = 4; 15 | const int allStreamSubscriptionCount = 3; 16 | 17 | for (var i = 0; i < streamSubscriptionCount; i++) 18 | await Fixture.Subscriptions.CreateToStreamAsync( 19 | stream, 20 | group + i, 21 | new(), 22 | userCredentials: TestCredentials.Root 23 | ); 24 | 25 | for (var i = 0; i < allStreamSubscriptionCount; i++) 26 | await Fixture.Subscriptions.CreateToAllAsync( 27 | group + i, 28 | new(), 29 | userCredentials: TestCredentials.Root 30 | ); 31 | 32 | var result = (await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root)).ToList(); 33 | Assert.Equal(allStreamSubscriptionCount, result.Count); 34 | Assert.All(result, s => Assert.Equal("$all", s.EventSource)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllWithoutPSObsoleteTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | using KurrentDB.Client.Tests.TestNode; 3 | using KurrentDB.Client.Tests; 4 | 5 | namespace KurrentDB.Client.Tests.PersistentSubscriptions; 6 | 7 | [Trait("Category", "Target:PersistentSubscriptions")] 8 | public class SubscribeToAllWithoutPSObsoleteTests(ITestOutputHelper output, KurrentDBTemporaryFixture fixture) 9 | : KurrentTemporaryTests(output, fixture) { 10 | [RetryFact] 11 | public async Task list_without_persistent_subscriptions() { 12 | await Assert.ThrowsAsync( 13 | async () => 14 | await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root) 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectToExistingWithStartFromNotSetTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | using KurrentDB.Client.Tests.TestNode; 3 | 4 | namespace KurrentDB.Client.Tests.PersistentSubscriptions; 5 | 6 | [Trait("Category", "Target:PersistentSubscriptions")] 7 | public class SubscribeToAllConnectToExistingWithStartFromNotSetTests( 8 | ITestOutputHelper output, 9 | KurrentDBTemporaryFixture fixture 10 | ) 11 | : KurrentTemporaryTests(output, fixture) { 12 | [RetryFact] 13 | public async Task connect_to_existing_with_start_from_not_set() { 14 | var group = Fixture.GetGroupName(); 15 | var stream = Fixture.GetStreamName(); 16 | 17 | foreach (var @event in Fixture.CreateTestEvents(10)) 18 | await Fixture.Streams.AppendToStreamAsync( 19 | stream, 20 | StreamState.Any, 21 | [@event] 22 | ); 23 | 24 | await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); 25 | await using var subscription = Fixture.Subscriptions.SubscribeToAll( 26 | group, 27 | new SubscribeToPersistentSubscriptionOptions { UserCredentials = TestCredentials.Root } 28 | ); 29 | 30 | await Assert.ThrowsAsync( 31 | () => subscription.Messages 32 | .OfType() 33 | .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) 34 | .AnyAsync() 35 | .AsTask() 36 | .WithTimeout() 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | 3 | namespace KurrentDB.Client.Tests.PersistentSubscriptions; 4 | 5 | [Trait("Category", "Target:PersistentSubscriptions")] 6 | public class SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests( 7 | ITestOutputHelper output, 8 | KurrentDBTemporaryFixture fixture 9 | ) 10 | : KurrentTemporaryTests(output, fixture) { 11 | [RetryFact] 12 | public async Task connect_to_existing_with_start_from_set_to_end_position() { 13 | var group = Fixture.GetGroupName(); 14 | var stream = Fixture.GetStreamName(); 15 | 16 | foreach (var @event in Fixture.CreateTestEvents(10)) { 17 | await Fixture.Streams.AppendToStreamAsync( 18 | stream, 19 | StreamState.Any, 20 | [@event] 21 | ); 22 | } 23 | 24 | await Fixture.Subscriptions.CreateToAllAsync( 25 | group, 26 | new(startFrom: Position.End), 27 | userCredentials: TestCredentials.Root 28 | ); 29 | 30 | var subscription = Fixture.Subscriptions.SubscribeToAll( 31 | group, 32 | new SubscribeToPersistentSubscriptionOptions { UserCredentials = TestCredentials.Root } 33 | ); 34 | 35 | await Assert.ThrowsAsync( 36 | () => subscription.Messages 37 | .OfType() 38 | .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) 39 | .AnyAsync() 40 | .AsTask() 41 | .WithTimeout() 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectWithoutReadPermissionsTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | 3 | namespace KurrentDB.Client.Tests.PersistentSubscriptions; 4 | 5 | [Trait("Category", "Target:PersistentSubscriptions")] 6 | public class SubscribeToAllConnectWithoutReadPermissionsTests( 7 | ITestOutputHelper output, 8 | KurrentDBTemporaryFixture fixture 9 | ) 10 | : KurrentTemporaryTests(output, fixture) { 11 | [RetryFact] 12 | public async Task connect_to_existing_without_read_all_permissions() { 13 | var group = Fixture.GetGroupName(); 14 | var user = Fixture.GetUserCredentials(); 15 | 16 | await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); 17 | 18 | await Fixture.DbUsers.CreateUserWithRetry( 19 | user.Username!, 20 | user.Username!, 21 | [], 22 | user.Password!, 23 | TestCredentials.Root 24 | ); 25 | 26 | await Assert.ThrowsAsync( 27 | async () => { 28 | await using var subscription = Fixture.Subscriptions.SubscribeToAll( 29 | group, 30 | new SubscribeToPersistentSubscriptionOptions { UserCredentials = user } 31 | ); 32 | 33 | await subscription.Messages.AnyAsync().AsTask().WithTimeout(); 34 | } 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllResultWithNormalUserCredentialsTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | using KurrentDB.Client.Tests; 3 | 4 | namespace KurrentDB.Client.Tests.PersistentSubscriptions; 5 | 6 | [Trait("Category", "Target:PersistentSubscriptions")] 7 | public class SubscribeToAllResultWithNormalUserCredentialsTests(ITestOutputHelper output, KurrentDBTemporaryFixture fixture) 8 | : KurrentTemporaryTests(output, fixture) { 9 | [RetryFact] 10 | public async Task returns_result_with_normal_user_credentials() { 11 | var group = Fixture.GetGroupName(); 12 | var stream = Fixture.GetStreamName(); 13 | 14 | const int streamSubscriptionCount = 4; 15 | const int allStreamSubscriptionCount = 3; 16 | 17 | for (var i = 0; i < streamSubscriptionCount; i++) 18 | await Fixture.Subscriptions.CreateToStreamAsync( 19 | stream, 20 | group + i, 21 | new(), 22 | userCredentials: TestCredentials.Root 23 | ); 24 | 25 | for (var i = 0; i < allStreamSubscriptionCount; i++) 26 | await Fixture.Subscriptions.CreateToAllAsync( 27 | group + i, 28 | new(), 29 | userCredentials: TestCredentials.Root 30 | ); 31 | 32 | var result = await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root); 33 | Assert.Equal(allStreamSubscriptionCount, result.Count()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReturnsAllSubscriptions.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | using KurrentDB.Client.Tests; 3 | 4 | namespace KurrentDB.Client.Tests.PersistentSubscriptions; 5 | 6 | [Trait("Category", "Target:PersistentSubscriptions")] 7 | public class SubscribeToAllReturnsAllSubscriptions(ITestOutputHelper output, SubscribeToAllReturnsAllSubscriptions.CustomFixture fixture) 8 | : KurrentTemporaryTests(output, fixture) { 9 | [RetryFact] 10 | public async Task returns_all_subscriptions() { 11 | var group = Fixture.GetGroupName(); 12 | var stream = Fixture.GetStreamName(); 13 | 14 | const int streamSubscriptionCount = 4; 15 | const int allStreamSubscriptionCount = 3; 16 | const int totalSubscriptionCount = streamSubscriptionCount + allStreamSubscriptionCount; 17 | 18 | for (var i = 0; i < streamSubscriptionCount; i++) 19 | await Fixture.Subscriptions.CreateToStreamAsync( 20 | stream, 21 | group + i, 22 | new(), 23 | userCredentials: TestCredentials.Root 24 | ); 25 | 26 | for (var i = 0; i < allStreamSubscriptionCount; i++) 27 | await Fixture.Subscriptions.CreateToAllAsync( 28 | group + i, 29 | new(), 30 | userCredentials: TestCredentials.Root 31 | ); 32 | 33 | var result = (await Fixture.Subscriptions.ListAllAsync(userCredentials: TestCredentials.Root)).ToList(); 34 | Assert.Equal(totalSubscriptionCount, result.Count); 35 | } 36 | 37 | public class CustomFixture : KurrentDBTemporaryFixture { 38 | public CustomFixture() { 39 | SkipPsWarmUp = true; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | using KurrentDB.Client.Tests; 3 | 4 | namespace KurrentDB.Client.Tests.PersistentSubscriptions; 5 | 6 | [Trait("Category", "Target:PersistentSubscriptions")] 7 | public class SubscribeToAllReturnsSubscriptionsToAllStreamTests(ITestOutputHelper output, KurrentDBTemporaryFixture fixture) 8 | : KurrentTemporaryTests(output, fixture) { 9 | [RetryFact] 10 | public async Task returns_subscriptions_to_all_stream() { 11 | var group = Fixture.GetGroupName(); 12 | var stream = Fixture.GetStreamName(); 13 | 14 | const int streamSubscriptionCount = 4; 15 | const int allStreamSubscriptionCount = 3; 16 | 17 | for (var i = 0; i < streamSubscriptionCount; i++) 18 | await Fixture.Subscriptions.CreateToStreamAsync( 19 | stream, 20 | group + i, 21 | new(), 22 | userCredentials: TestCredentials.Root 23 | ); 24 | 25 | for (var i = 0; i < allStreamSubscriptionCount; i++) 26 | await Fixture.Subscriptions.CreateToAllAsync( 27 | group + i, 28 | new(), 29 | userCredentials: TestCredentials.Root 30 | ); 31 | 32 | var result = (await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root)).ToList(); 33 | Assert.Equal(allStreamSubscriptionCount, result.Count); 34 | Assert.All(result, s => Assert.Equal("$all", s.EventSource)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllWithoutPSTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | using KurrentDB.Client.Tests.TestNode; 3 | using KurrentDB.Client.Tests; 4 | 5 | namespace KurrentDB.Client.Tests.PersistentSubscriptions; 6 | 7 | [Trait("Category", "Target:PersistentSubscriptions")] 8 | public class SubscribeToAllWithoutPsTests(ITestOutputHelper output, KurrentDBTemporaryFixture fixture) 9 | : KurrentTemporaryTests(output, fixture) { 10 | [RetryFact] 11 | public async Task list_without_persistent_subscriptions() { 12 | await Assert.ThrowsAsync( 13 | async () => 14 | await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root) 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamConnectToExistingWithoutPermissionObsoleteTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | using KurrentDB.Client.Tests.TestNode; 3 | 4 | namespace KurrentDB.Client.Tests.PersistentSubscriptions; 5 | 6 | [Trait("Category", "Target:PersistentSubscriptions")] 7 | public class SubscribeToStreamConnectToExistingWithoutPermissionObsoleteTests( 8 | ITestOutputHelper output, 9 | SubscribeToStreamConnectToExistingWithoutPermissionObsoleteTests.CustomFixture fixture 10 | ) 11 | : KurrentTemporaryTests(output, fixture) { 12 | [Fact] 13 | public async Task connect_to_existing_without_permissions() { 14 | var stream = Fixture.GetStreamName(); 15 | var group = Fixture.GetGroupName(); 16 | 17 | await Fixture.Subscriptions.CreateToStreamAsync( 18 | stream, 19 | group, 20 | new(), 21 | userCredentials: TestCredentials.Root 22 | ); 23 | 24 | await Assert.ThrowsAsync( 25 | async () => { 26 | using var _ = await Fixture.Subscriptions.SubscribeToStreamAsync( 27 | stream, 28 | group, 29 | delegate { return Task.CompletedTask; } 30 | ); 31 | } 32 | ).WithTimeout(); 33 | } 34 | 35 | public class CustomFixture() : KurrentDBTemporaryFixture(x => x.WithoutDefaultCredentials()); 36 | } 37 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamConnectToExistingWithStartFromBeginningTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | using KurrentDB.Client.Tests.TestNode; 3 | 4 | namespace KurrentDB.Client.Tests.PersistentSubscriptions; 5 | 6 | [Trait("Category", "Target:PersistentSubscriptions")] 7 | public class SubscribeToStreamConnectToExistingWithStartFromBeginningTests(ITestOutputHelper output, KurrentDBTemporaryFixture fixture) 8 | : KurrentTemporaryTests(output, fixture) { 9 | [RetryFact] 10 | public async Task connect_to_existing_with_start_from_beginning_and_no_streamconnect_to_existing_with_start_from_not_set_and_events_in_it() { 11 | var stream = Fixture.GetStreamName(); 12 | var group = Fixture.GetGroupName(); 13 | var events = Fixture.CreateTestEvents(10).ToArray(); 14 | 15 | await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); 16 | await Fixture.Subscriptions.CreateToStreamAsync( 17 | stream, 18 | group, 19 | new(), 20 | userCredentials: TestCredentials.Root 21 | ); 22 | 23 | await using var subscription = Fixture.Subscriptions.SubscribeToStream( 24 | stream, 25 | group, 26 | new SubscribeToPersistentSubscriptionOptions { UserCredentials = TestCredentials.Root } 27 | ); 28 | 29 | await Assert.ThrowsAsync( 30 | () => subscription.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event).AsTask().WithTimeout() 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamListTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | using KurrentDB.Client.Tests.TestNode; 3 | 4 | namespace KurrentDB.Client.Tests.PersistentSubscriptions; 5 | 6 | [Trait("Category", "Target:PersistentSubscriptions")] 7 | public class SubscribeToStreamListTests(ITestOutputHelper output, SubscribeToStreamListTests.CustomFixture fixture) 8 | : KurrentTemporaryTests(output, fixture) { 9 | [RetryFact] 10 | public async Task throws_with_no_credentials() { 11 | var stream = Fixture.GetStreamName(); 12 | var group = Fixture.GetGroupName(); 13 | 14 | const int streamSubscriptionCount = 4; 15 | 16 | for (var i = 0; i < streamSubscriptionCount; i++) 17 | await Fixture.Subscriptions.CreateToStreamAsync( 18 | stream, 19 | group + i, 20 | new(), 21 | userCredentials: TestCredentials.Root 22 | ); 23 | 24 | await Assert.ThrowsAsync(async () => await Fixture.Subscriptions.ListToStreamAsync(stream)); 25 | } 26 | 27 | [RetryFact] 28 | public async Task throws_with_non_existing_user() { 29 | var stream = Fixture.GetStreamName(); 30 | var group = Fixture.GetGroupName(); 31 | 32 | const int streamSubscriptionCount = 4; 33 | 34 | for (var i = 0; i < streamSubscriptionCount; i++) 35 | await Fixture.Subscriptions.CreateToStreamAsync( 36 | stream, 37 | group + i, 38 | new(), 39 | userCredentials: TestCredentials.Root 40 | ); 41 | 42 | await Assert.ThrowsAsync( 43 | async () => await Fixture.Subscriptions.ListToStreamAsync(stream, userCredentials: TestCredentials.TestBadUser) 44 | ); 45 | } 46 | 47 | public class CustomFixture() : KurrentDBTemporaryFixture(x => x.WithoutDefaultCredentials()); 48 | } 49 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/ProjectionManagement/DisableProjectionTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | 3 | namespace KurrentDB.Client.Tests.Projections; 4 | 5 | [Trait("Category", "Target:ProjectionManagement")] 6 | public class DisableProjectionTests(ITestOutputHelper output, DisableProjectionTests.CustomFixture fixture) 7 | : KurrentTemporaryTests(output, fixture) { 8 | [Fact] 9 | public async Task disable_projection() { 10 | var name = Names.First(); 11 | await Fixture.DbProjections.DisableAsync(name, userCredentials: TestCredentials.Root); 12 | var result = await Fixture.DbProjections.GetStatusAsync(name, userCredentials: TestCredentials.Root); 13 | Assert.NotNull(result); 14 | Assert.Contains(["Aborted/Stopped", "Stopped"], x => x == result!.Status); 15 | } 16 | 17 | static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; 18 | 19 | public class CustomFixture : KurrentDBTemporaryFixture { 20 | public CustomFixture() : base(x => x.RunProjections()) { } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/ProjectionManagement/EnableProjectionTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | 3 | namespace KurrentDB.Client.Tests.Projections; 4 | 5 | [Trait("Category", "Target:ProjectionManagement")] 6 | public class EnableProjectionTests(ITestOutputHelper output, EnableProjectionTests.CustomFixture fixture) 7 | : KurrentTemporaryTests(output, fixture) { 8 | [Fact] 9 | public async Task enable_projection() { 10 | var name = Names.First(); 11 | await Fixture.DbProjections.EnableAsync(name, userCredentials: TestCredentials.Root); 12 | var result = await Fixture.DbProjections.GetStatusAsync(name, userCredentials: TestCredentials.Root); 13 | Assert.NotNull(result); 14 | Assert.Equal("Running", result.Status); 15 | } 16 | 17 | static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; 18 | 19 | public class CustomFixture : KurrentDBTemporaryFixture { 20 | public CustomFixture() : base(x => x.RunProjections()) { } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/ProjectionManagement/GetProjectionResultTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | using KurrentDB.Client.Tests.TestNode; 3 | 4 | namespace KurrentDB.Client.Tests.Projections; 5 | 6 | [Trait("Category", "Target:ProjectionManagement")] 7 | public class GetProjectionResultTests(ITestOutputHelper output, GetProjectionResultTests.CustomFixture fixture) 8 | : KurrentTemporaryTests(output, fixture) { 9 | [Fact] 10 | public async Task get_result() { 11 | var name = Fixture.GetProjectionName(); 12 | Result? result = null; 13 | 14 | var projection = $$""" 15 | fromStream('{{name}}').when({ 16 | "$init": function() { return { Count: 0 }; }, 17 | "$any": function(s, e) { s.Count++; return s; } 18 | }); 19 | """; 20 | 21 | await Fixture.DbProjections.CreateContinuousAsync( 22 | name, 23 | projection, 24 | userCredentials: TestCredentials.Root 25 | ); 26 | 27 | await Fixture.Streams.AppendToStreamAsync( 28 | name, 29 | StreamState.NoStream, 30 | Fixture.CreateTestEvents() 31 | ); 32 | 33 | await AssertEx.IsOrBecomesTrue( 34 | async () => { 35 | result = await Fixture.DbProjections.GetResultAsync(name, userCredentials: TestCredentials.Root); 36 | return result.Count > 0; 37 | } 38 | ); 39 | 40 | Assert.NotNull(result); 41 | Assert.Equal(1, result!.Count); 42 | } 43 | 44 | record Result { 45 | public int Count { get; set; } 46 | } 47 | 48 | public class CustomFixture : KurrentDBTemporaryFixture { 49 | public CustomFixture() : base(x => x.RunProjections()) { } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/ProjectionManagement/GetProjectionStateTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | using KurrentDB.Client.Tests.TestNode; 3 | 4 | namespace KurrentDB.Client.Tests.Projections; 5 | 6 | [Trait("Category", "Target:ProjectionManagement")] 7 | public class GetProjectionStateTests(ITestOutputHelper output, GetProjectionStateTests.CustomFixture fixture) 8 | : KurrentTemporaryTests(output, fixture) { 9 | [Fact] 10 | public async Task get_state() { 11 | var name = Fixture.GetProjectionName(); 12 | 13 | var projection = $$""" 14 | fromStream('{{name}}').when({ 15 | "$init": function() { return { Count: 0 }; }, 16 | "$any": function(s, e) { s.Count++; return s; } 17 | }); 18 | """; 19 | 20 | Result? result = null; 21 | 22 | await Fixture.DbProjections.CreateContinuousAsync( 23 | name, 24 | projection, 25 | userCredentials: TestCredentials.Root 26 | ); 27 | 28 | await Fixture.Streams.AppendToStreamAsync( 29 | name, 30 | StreamState.NoStream, 31 | Fixture.CreateTestEvents() 32 | ); 33 | 34 | await AssertEx.IsOrBecomesTrue( 35 | async () => { 36 | result = await Fixture.DbProjections.GetStateAsync(name, userCredentials: TestCredentials.Root); 37 | return result.Count > 0; 38 | } 39 | ); 40 | 41 | Assert.NotNull(result); 42 | Assert.Equal(1, result!.Count); 43 | } 44 | 45 | record Result { 46 | public int Count { get; set; } 47 | } 48 | 49 | public class CustomFixture : KurrentDBTemporaryFixture { 50 | public CustomFixture() : base(x => x.RunProjections()) { } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/ProjectionManagement/GetProjectionStatusTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | 3 | namespace KurrentDB.Client.Tests.Projections; 4 | 5 | [Trait("Category", "Target:ProjectionManagement")] 6 | public class GetProjectionStatusTests(ITestOutputHelper output, GetProjectionStatusTests.CustomFixture fixture) 7 | : KurrentTemporaryTests(output, fixture) { 8 | [Fact] 9 | public async Task get_status() { 10 | var name = Names.First(); 11 | var result = await Fixture.DbProjections.GetStatusAsync(name, userCredentials: TestCredentials.Root); 12 | 13 | Assert.NotNull(result); 14 | Assert.Equal(name, result.Name); 15 | } 16 | 17 | static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; 18 | 19 | public class CustomFixture : KurrentDBTemporaryFixture { 20 | public CustomFixture() : base(x => x.RunProjections()) { } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/ProjectionManagement/ListAllProjectionsTests.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | 3 | using KurrentDB.Client.Tests.TestNode; 4 | 5 | namespace KurrentDB.Client.Tests; 6 | 7 | [Trait("Category", "Target:ProjectionManagement")] 8 | public class ListAllProjectionsTests(ITestOutputHelper output, ListAllProjectionsTests.CustomFixture fixture) 9 | : KurrentTemporaryTests(output, fixture) { 10 | [Fact] 11 | public async Task list_continuous_projections() { 12 | var name = Fixture.GetProjectionName(); 13 | 14 | await Fixture.DbProjections.CreateContinuousAsync( 15 | name, 16 | "fromAll().when({$init: function (state, ev) {return {};}});", 17 | userCredentials: TestCredentials.Root 18 | ); 19 | 20 | var result = await Fixture.DbProjections.ListContinuousAsync(userCredentials: TestCredentials.Root) 21 | .ToArrayAsync(); 22 | 23 | Assert.Equal( 24 | result.Select(x => x.Name).OrderBy(x => x), 25 | Names.Concat([name]).OrderBy(x => x) 26 | ); 27 | 28 | Assert.True(result.All(x => x.Mode == "Continuous")); 29 | } 30 | 31 | static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; 32 | 33 | public class CustomFixture : KurrentDBTemporaryFixture { 34 | public CustomFixture() : base(x => x.RunProjections()) { } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/ProjectionManagement/ListContinuousProjectionsTests.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | 3 | using KurrentDB.Client.Tests.TestNode; 4 | 5 | namespace KurrentDB.Client.Tests; 6 | 7 | [Trait("Category", "Target:ProjectionManagement")] 8 | public class ListContinuousProjectionsTests(ITestOutputHelper output, ListContinuousProjectionsTests.CustomFixture fixture) 9 | : KurrentTemporaryTests(output, fixture) { 10 | [Fact] 11 | public async Task list_continuous_projections() { 12 | var name = Fixture.GetProjectionName(); 13 | 14 | await Fixture.DbProjections.CreateContinuousAsync( 15 | name, 16 | "fromAll().when({$init: function (state, ev) {return {};}});", 17 | userCredentials: TestCredentials.Root 18 | ); 19 | 20 | var result = await Fixture.DbProjections.ListContinuousAsync(userCredentials: TestCredentials.Root) 21 | .ToArrayAsync(); 22 | 23 | Assert.Equal( 24 | result.Select(x => x.Name).OrderBy(x => x), 25 | Names.Concat([name]).OrderBy(x => x) 26 | ); 27 | 28 | Assert.True(result.All(x => x.Mode == "Continuous")); 29 | } 30 | 31 | static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; 32 | 33 | public class CustomFixture : KurrentDBTemporaryFixture { 34 | public CustomFixture() : base(x => x.RunProjections()) { } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/ProjectionManagement/ListOneTimeProjectionsTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | 3 | namespace KurrentDB.Client.Tests.Projections; 4 | 5 | [Trait("Category", "Target:ProjectionManagement")] 6 | public class ListOneTimeProjectionsTests(ITestOutputHelper output, ListOneTimeProjectionsTests.CustomFixture fixture) 7 | : KurrentTemporaryTests(output, fixture) { 8 | [Fact] 9 | public async Task list_one_time_projections() { 10 | await Fixture.DbProjections.CreateOneTimeAsync("fromAll().when({$init: function (state, ev) {return {};}});", userCredentials: TestCredentials.Root); 11 | 12 | var result = await Fixture.DbProjections.ListOneTimeAsync(userCredentials: TestCredentials.Root) 13 | .ToArrayAsync(); 14 | 15 | var details = Assert.Single(result); 16 | Assert.Equal("OneTime", details.Mode); 17 | } 18 | 19 | public class CustomFixture : KurrentDBTemporaryFixture { 20 | public CustomFixture() : base(x => x.RunProjections()) { } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/ProjectionManagement/ResetProjectionTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | 3 | namespace KurrentDB.Client.Tests.Projections; 4 | 5 | [Trait("Category", "Target:ProjectionManagement")] 6 | public class ResetProjectionTests(ITestOutputHelper output, ResetProjectionTests.CustomFixture fixture) 7 | : KurrentTemporaryTests(output, fixture) { 8 | [Fact] 9 | public async Task reset_projection() { 10 | var name = Names.First(); 11 | await Fixture.DbProjections.ResetAsync(name, userCredentials: TestCredentials.Root); 12 | var result = await Fixture.DbProjections.GetStatusAsync(name, userCredentials: TestCredentials.Root); 13 | 14 | Assert.NotNull(result); 15 | Assert.Equal("Running", result.Status); 16 | } 17 | 18 | static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; 19 | 20 | public class CustomFixture : KurrentDBTemporaryFixture { 21 | public CustomFixture() : base(x => x.RunProjections()) { } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/ProjectionManagement/RestartSubsystemTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | using KurrentDB.Client.Tests.TestNode; 3 | 4 | namespace KurrentDB.Client.Tests.Projections; 5 | 6 | [Trait("Category", "Target:ProjectionManagement")] 7 | public class RestartSubsystemTests(ITestOutputHelper output, RestartSubsystemTests.CustomFixture fixture) 8 | : KurrentTemporaryTests(output, fixture) { 9 | [Fact] 10 | public async Task restart_subsystem_does_not_throw() => 11 | await Fixture.DbProjections.RestartSubsystemAsync(userCredentials: TestCredentials.Root); 12 | 13 | [Fact] 14 | public async Task restart_subsystem_throws_when_given_no_credentials() => 15 | await Assert.ThrowsAsync(() => Fixture.DbProjections.RestartSubsystemAsync(userCredentials: TestCredentials.TestUser1)); 16 | 17 | public class CustomFixture : KurrentDBTemporaryFixture { 18 | public CustomFixture() : base(x => x.RunProjections()) { } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/ProjectionManagement/UpdateProjectionTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client.Tests.TestNode; 2 | 3 | namespace KurrentDB.Client.Tests.Projections; 4 | 5 | [Trait("Category", "Target:ProjectionManagement")] 6 | public class UpdateProjectionTests(ITestOutputHelper output, UpdateProjectionTests.CustomFixture fixture) 7 | : KurrentTemporaryTests(output, fixture) { 8 | [Theory] 9 | [InlineData(true)] 10 | [InlineData(false)] 11 | [InlineData(null)] 12 | public async Task update_projection(bool? emitEnabled) { 13 | var name = Fixture.GetProjectionName(); 14 | await Fixture.DbProjections.CreateContinuousAsync( 15 | name, 16 | "fromAll().when({$init: function (state, ev) {return {};}});", 17 | userCredentials: TestCredentials.Root 18 | ); 19 | 20 | await Fixture.DbProjections.UpdateAsync( 21 | name, 22 | "fromAll().when({$init: function (s, e) {return {};}});", 23 | emitEnabled, 24 | userCredentials: TestCredentials.Root 25 | ); 26 | } 27 | 28 | public class CustomFixture : KurrentDBTemporaryFixture { 29 | public CustomFixture() : base(x => x.RunProjections()) { } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/RegularFilterExpressionTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using AutoFixture; 3 | using KurrentDB.Client; 4 | 5 | namespace KurrentDB.Client.Tests; 6 | 7 | [Trait("Category", "Target:Misc")] 8 | public class RegularFilterExpressionTests : ValueObjectTests { 9 | public RegularFilterExpressionTests() : base(new ScenarioFixture()) { } 10 | 11 | class ScenarioFixture : Fixture { 12 | public ScenarioFixture() => Customize(composer => composer.FromFactory(value => new(value))); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Security/ReadAllSecurityTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | using KurrentDB.Client.Tests.TestNode; 3 | using KurrentDB.Client.Tests; 4 | 5 | namespace KurrentDB.Client.Tests; 6 | 7 | [Trait("Category", "Target:Security")] 8 | public class ReadAllSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { 9 | [Fact] 10 | public async Task reading_all_with_not_existing_credentials_is_not_authenticated() { 11 | await Assert.ThrowsAsync(() => Fixture.ReadAllForward(TestCredentials.TestBadUser)); 12 | await Assert.ThrowsAsync(() => Fixture.ReadAllBackward(TestCredentials.TestBadUser)); 13 | } 14 | 15 | [Fact] 16 | public async Task reading_all_with_no_credentials_is_denied() { 17 | await Assert.ThrowsAsync(() => Fixture.ReadAllForward()); 18 | await Assert.ThrowsAsync(() => Fixture.ReadAllBackward()); 19 | } 20 | 21 | [Fact] 22 | public async Task reading_all_with_not_authorized_user_credentials_is_denied() { 23 | await Assert.ThrowsAsync(() => Fixture.ReadAllForward(TestCredentials.TestUser2)); 24 | await Assert.ThrowsAsync(() => Fixture.ReadAllBackward(TestCredentials.TestUser2)); 25 | } 26 | 27 | [Fact] 28 | public async Task reading_all_with_authorized_user_credentials_succeeds() { 29 | await Fixture.ReadAllForward(TestCredentials.TestUser1); 30 | await Fixture.ReadAllBackward(TestCredentials.TestUser1); 31 | } 32 | 33 | [Fact] 34 | public async Task reading_all_with_admin_credentials_succeeds() { 35 | await Fixture.ReadAllForward(TestCredentials.TestAdmin); 36 | await Fixture.ReadAllBackward(TestCredentials.TestAdmin); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Security/SubscribeToAllSecurityTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | using KurrentDB.Client.Tests.TestNode; 3 | using KurrentDB.Client.Tests; 4 | 5 | namespace KurrentDB.Client.Tests; 6 | 7 | [Trait("Category", "Target:Security")] 8 | public class SubscribeToAllSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { 9 | [Fact] 10 | public async Task subscribing_to_all_with_not_existing_credentials_is_not_authenticated() => 11 | await Assert.ThrowsAsync(() => Fixture.SubscribeToAll(TestCredentials.TestBadUser)); 12 | 13 | [Fact] 14 | public async Task subscribing_to_all_with_no_credentials_is_denied() => await Assert.ThrowsAsync(() => Fixture.SubscribeToAll()); 15 | 16 | [Fact] 17 | public async Task subscribing_to_all_with_not_authorized_user_credentials_is_denied() => 18 | await Assert.ThrowsAsync(() => Fixture.SubscribeToAll(TestCredentials.TestUser2)); 19 | 20 | [Fact] 21 | public async Task subscribing_to_all_with_authorized_user_credentials_succeeds() => await Fixture.SubscribeToAll(TestCredentials.TestUser1); 22 | 23 | [Fact] 24 | public async Task subscribing_to_all_with_admin_user_credentials_succeeds() => await Fixture.SubscribeToAll(TestCredentials.TestAdmin); 25 | } 26 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Streams/Read/MessageBinaryData.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client.Tests; 2 | 3 | public readonly record struct MessageBinaryData(Uuid Id, byte[] Data, byte[] Metadata) { 4 | public bool Equals(MessageBinaryData other) => 5 | Id.Equals(other.Id) 6 | && Data.SequenceEqual(other.Data) 7 | && Metadata.SequenceEqual(other.Metadata); 8 | 9 | public override int GetHashCode() => System.HashCode.Combine(Id, Data, Metadata); 10 | } 11 | 12 | public static class EventBinaryDataConverters { 13 | public static MessageBinaryData ToBinaryData(this MessageData source) => 14 | new(source.MessageId, source.Data.ToArray(), source.Metadata.ToArray()); 15 | 16 | public static MessageBinaryData ToBinaryData(this EventRecord source) => 17 | new(source.EventId, source.Data.ToArray(), source.Metadata.ToArray()); 18 | 19 | public static MessageBinaryData ToBinaryData(this ResolvedEvent source) => 20 | source.Event.ToBinaryData(); 21 | 22 | public static MessageBinaryData[] ToBinaryData(this IEnumerable source) => 23 | source.Select(x => x.ToBinaryData()).ToArray(); 24 | 25 | public static MessageBinaryData[] ToBinaryData(this IEnumerable source) => 26 | source.Select(x => x.ToBinaryData()).ToArray(); 27 | 28 | public static MessageBinaryData[] ToBinaryData(this IEnumerable source) => 29 | source.Select(x => x.ToBinaryData()).ToArray(); 30 | 31 | public static ValueTask ToBinaryData(this IAsyncEnumerable source) => 32 | source.DefaultIfEmpty().Select(x => x.ToBinaryData()).ToArrayAsync(); 33 | } 34 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Streams/Read/MessageDataComparer.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | 3 | namespace KurrentDB.Client.Tests; 4 | 5 | static class MessageDataComparer { 6 | public static bool Equal(MessageData expected, EventRecord actual) { 7 | if (expected.MessageId != actual.EventId) 8 | return false; 9 | 10 | if (expected.Type != actual.EventType) 11 | return false; 12 | 13 | return expected.Data.ToArray().SequenceEqual(actual.Data.ToArray()) 14 | && expected.Metadata.ToArray().SequenceEqual(actual.Metadata.ToArray()); 15 | } 16 | 17 | public static bool Equal(MessageData[] expected, EventRecord[] actual) { 18 | if (expected.Length != actual.Length) 19 | return false; 20 | 21 | return !expected.Where((t, i) => !Equal(t, actual[i])).Any(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Streams/Read/ReadAllEventsFixture.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | using KurrentDB.Client.Tests.TestNode; 3 | 4 | namespace KurrentDB.Client.Tests; 5 | 6 | [Trait("Category", "Target:Streams")] 7 | [Trait("Category", "Target:All")] 8 | [Trait("Category", "Operation:Read")] 9 | [Trait("Category", "Database:Dedicated")] 10 | public class ReadAllEventsFixture : KurrentDBTemporaryFixture { 11 | public ReadAllEventsFixture() { 12 | OnSetup = async () => { 13 | _ = await Streams.SetStreamMetadataAsync( 14 | SystemStreams.AllStream, 15 | StreamState.NoStream, 16 | new(acl: new(SystemRoles.All)), 17 | new SetStreamMetadataOptions { UserCredentials = TestCredentials.Root } 18 | ); 19 | 20 | Events = CreateTestEvents(20) 21 | .Concat(CreateTestEvents(2, metadata: CreateMetadataOfSize(10_000))) 22 | .Concat(CreateTestEvents(2, AnotherTestEventType)) 23 | .ToArray(); 24 | 25 | ExpectedStreamName = GetStreamName(); 26 | 27 | await Streams.AppendToStreamAsync(ExpectedStreamName, StreamState.NoStream, Events); 28 | 29 | ExpectedEvents = Events.ToBinaryData(); 30 | ExpectedEventsReversed = Enumerable.Reverse(ExpectedEvents).ToArray(); 31 | 32 | ExpectedFirstEvent = ExpectedEvents.First(); 33 | ExpectedLastEvent = ExpectedEvents.Last(); 34 | }; 35 | } 36 | 37 | public string ExpectedStreamName { get; private set; } = null!; 38 | 39 | public MessageData[] Events { get; private set; } = []; 40 | 41 | public MessageBinaryData[] ExpectedEvents { get; private set; } = []; 42 | public MessageBinaryData[] ExpectedEventsReversed { get; private set; } = []; 43 | 44 | public MessageBinaryData ExpectedFirstEvent { get; private set; } 45 | public MessageBinaryData ExpectedLastEvent { get; private set; } 46 | } 47 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Streams/SubscriptionFilter.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | 3 | using KurrentDB.Client; 4 | 5 | namespace KurrentDB.Client.Tests.Streams; 6 | 7 | public record SubscriptionFilter(string Name, Func Create, Func PrepareEvent) { 8 | public override string ToString() => Name; 9 | 10 | static readonly SubscriptionFilter StreamNamePrefix = new(nameof(StreamNamePrefix), StreamFilter.Prefix, (_, evt) => evt); 11 | static readonly SubscriptionFilter StreamNameRegex = new(nameof(StreamNameRegex), f => StreamFilter.RegularExpression(f), (_, evt) => evt); 12 | static readonly SubscriptionFilter EventTypePrefix = new(nameof(EventTypePrefix), EventTypeFilter.Prefix, (term, evt) => new(term, evt.Data, evt.Metadata, evt.MessageId, evt.ContentType)); 13 | static readonly SubscriptionFilter EventTypeRegex = new(nameof(EventTypeRegex), f => EventTypeFilter.RegularExpression(f), (term, evt) => new(term, evt.Data, evt.Metadata, evt.MessageId, evt.ContentType)); 14 | 15 | static SubscriptionFilter() { 16 | All = new[] { 17 | StreamNamePrefix, 18 | StreamNameRegex, 19 | EventTypePrefix, 20 | EventTypeRegex 21 | }; 22 | 23 | TestCases = All.Select(x => new object[] { x }); 24 | } 25 | 26 | public static SubscriptionFilter[] All { get; } 27 | public static IEnumerable TestCases { get; } 28 | } 29 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/Streams/Subscriptions/SubscriptionDroppedResult.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | 3 | namespace KurrentDB.Client.Tests; 4 | 5 | public record SubscriptionDroppedResult(SubscriptionDroppedReason Reason, Exception? Error) { 6 | public Task Throw() => Task.FromException(Error!); 7 | 8 | public static SubscriptionDroppedResult ServerError(Exception? error = null) => 9 | new(SubscriptionDroppedReason.ServerError, error ?? new Exception("Server error")); 10 | 11 | public static SubscriptionDroppedResult SubscriberError(Exception? error = null) => 12 | new(SubscriptionDroppedReason.SubscriberError, error ?? new Exception("Subscriber error")); 13 | 14 | public static SubscriptionDroppedResult Disposed(Exception? error = null) => 15 | new(SubscriptionDroppedReason.Disposed, error); 16 | 17 | public override string ToString() => $"{Reason} {Error?.Message ?? string.Empty}".Trim(); 18 | } 19 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/UserManagement/GetCurrentUserTests.cs: -------------------------------------------------------------------------------- 1 | using KurrentDB.Client; 2 | 3 | namespace KurrentDB.Client.Tests; 4 | 5 | [Trait("Category", "Target:UserManagement")] 6 | public class GetCurrentUserTests(ITestOutputHelper output, KurrentDBPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { 7 | [Fact] 8 | public async Task returns_the_current_user() { 9 | var user = await Fixture.DbUsers.GetCurrentUserAsync(TestCredentials.Root); 10 | user.LoginName.ShouldBe(TestCredentials.Root.Username); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/UserManagement/ListUserTests.cs: -------------------------------------------------------------------------------- 1 | namespace KurrentDB.Client.Tests; 2 | 3 | [Trait("Category", "Target:UserManagement")] 4 | public class ListUserTests(ITestOutputHelper output, KurrentDBPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { 5 | readonly string _userFullNamePrefix = fixture.IsKdb ? "KurrentDB" : "Event Store"; 6 | 7 | [Fact] 8 | public async Task returns_all_created_users() { 9 | var seed = await Fixture.CreateTestUsers(); 10 | 11 | var admin = new UserDetails("admin", $"{_userFullNamePrefix} Administrator", new[] { "$admins" }, false, default); 12 | var ops = new UserDetails("ops", $"{_userFullNamePrefix} Operations", new[] { "$ops" }, false, default); 13 | 14 | var expected = new[] { admin, ops } 15 | .Concat(seed.Select(user => user.Details)) 16 | .ToArray(); 17 | 18 | var actual = await Fixture.DbUsers 19 | .ListAllAsync(userCredentials: TestCredentials.Root) 20 | .Select(user => new UserDetails(user.LoginName, user.FullName, user.Groups, user.Disabled, default)) 21 | .ToArrayAsync(); 22 | 23 | expected.ShouldBeSubsetOf(actual); 24 | } 25 | 26 | [Fact] 27 | public async Task returns_all_system_users() { 28 | var admin = new UserDetails("admin", $"{_userFullNamePrefix} Administrator", new[] { "$admins" }, false, default); 29 | var ops = new UserDetails("ops", $"{_userFullNamePrefix} Operations", new[] { "$ops" }, false, default); 30 | 31 | var expected = new[] { admin, ops }; 32 | 33 | var actual = await Fixture.DbUsers 34 | .ListAllAsync(userCredentials: TestCredentials.Root) 35 | .Select(user => new UserDetails(user.LoginName, user.FullName, user.Groups, user.Disabled, default)) 36 | .ToArrayAsync(); 37 | 38 | expected.ShouldBeSubsetOf(actual); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/UserManagement/UserCredentialsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using System.Text; 3 | using KurrentDB.Client; 4 | using static System.Convert; 5 | 6 | namespace KurrentDB.Client.Tests; 7 | 8 | [Trait("Category", "Target:UserManagement")] 9 | public class UserCredentialsTests { 10 | const string JwtToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." 11 | + "eyJzdWIiOiI5OSIsIm5hbWUiOiJKb2huIFdpY2siLCJpYXQiOjE1MTYyMzkwMjJ9." 12 | + "MEdv44JIdlLh-GgqxOTZD7DHq28xJowhQFmDnT3NDIE"; 13 | 14 | static readonly UTF8Encoding Utf8NoBom = new(false); 15 | 16 | static string EncodeCredentials(string username, string password) => ToBase64String(Utf8NoBom.GetBytes($"{username}:{password}")); 17 | 18 | [Fact] 19 | public void from_username_and_password() { 20 | var user = Fakers.Users.WithNonAsciiPassword(); 21 | 22 | var value = new AuthenticationHeaderValue( 23 | Constants.Headers.BasicScheme, 24 | EncodeCredentials(user.LoginName, user.Password) 25 | ); 26 | 27 | var basicAuthInfo = value.ToString(); 28 | 29 | var credentials = new UserCredentials(user.LoginName, user.Password); 30 | 31 | credentials.Username.ShouldBe(user.LoginName); 32 | credentials.Password.ShouldBe(user.Password); 33 | credentials.ToString().ShouldBe(basicAuthInfo); 34 | } 35 | 36 | [Fact] 37 | public void from_bearer_token() { 38 | var credentials = new UserCredentials(JwtToken); 39 | 40 | credentials.Username.ShouldBeNull(); 41 | credentials.Password.ShouldBeNull(); 42 | credentials.ToString().ShouldBe($"Bearer {JwtToken}"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/ValueObjectTests.cs: -------------------------------------------------------------------------------- 1 | using AutoFixture; 2 | 3 | namespace KurrentDB.Client.Tests; 4 | 5 | [Trait("Category", "Target:Misc")] 6 | public abstract class ValueObjectTests { 7 | protected readonly Fixture _fixture; 8 | 9 | protected ValueObjectTests(Fixture fixture) => _fixture = fixture; 10 | 11 | [RetryFact] 12 | public void ValueObjectIsWellBehaved() => _fixture.Create().Verify(typeof(T)); 13 | 14 | [RetryFact] 15 | public void ValueObjectIsEquatable() => Assert.IsAssignableFrom>(_fixture.Create()); 16 | } 17 | -------------------------------------------------------------------------------- /test/KurrentDB.Client.Tests/X509CertificatesTests.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography.X509Certificates; 2 | using KurrentDB.Client; 3 | 4 | namespace KurrentDB.Client.Tests; 5 | 6 | [Trait("Category", "Target:Misc")] 7 | public class X509CertificatesTests { 8 | [RetryFact] 9 | public void create_from_pem_file() { 10 | const string certPemFilePath = "certs/ca/ca.crt"; 11 | const string keyPemFilePath = "certs/ca/ca.key"; 12 | 13 | var rsa = X509Certificates.CreateFromPemFile(certPemFilePath, keyPemFilePath); 14 | 15 | #if NET9_0_OR_GREATER 16 | var cert = X509CertificateLoader.LoadCertificateFromFile(certPemFilePath); 17 | #else 18 | var cert = new X509Certificate2(certPemFilePath); 19 | #endif 20 | 21 | rsa.Issuer.ShouldBe(cert.Issuer); 22 | rsa.SerialNumber.ShouldBe(cert.SerialNumber); 23 | } 24 | 25 | [RetryFact] 26 | public void create_from_pem_file_() { 27 | const string certPemFilePath = "certs/user-admin/user-admin.crt"; 28 | const string keyPemFilePath = "certs/user-admin/user-admin.key"; 29 | 30 | var rsa = X509Certificates.CreateFromPemFile(certPemFilePath, keyPemFilePath); 31 | 32 | #if NET9_0_OR_GREATER 33 | var cert = X509CertificateLoader.LoadCertificateFromFile(certPemFilePath); 34 | #else 35 | var cert = new X509Certificate2(certPemFilePath); 36 | #endif 37 | 38 | rsa.Issuer.ShouldBe(cert.Issuer); 39 | rsa.Subject.ShouldBe(cert.Subject); 40 | rsa.SerialNumber.ShouldBe(cert.SerialNumber); 41 | } 42 | } 43 | --------------------------------------------------------------------------------