diff --git a/Directory.Build.props b/Directory.Build.props index eecbb8220..5c261f63f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - net6.0;net7.0;net8.0 + net48;net6.0;net7.0;net8.0 true enable enable @@ -16,4 +16,9 @@ 2.59.0 2.59.0 + + + + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 1921131a9..c12d9449c 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,5 +1,5 @@ - + EventStore.Client @@ -12,8 +12,8 @@ - - + + @@ -30,8 +30,8 @@ - - + + @@ -43,11 +43,11 @@ all runtime; build; native; contentfiles; analyzers - + - + diff --git a/src/EventStore.Client.Common/EpochExtensions.cs b/src/EventStore.Client.Common/EpochExtensions.cs index d62bdb44c..db59e620d 100644 --- a/src/EventStore.Client.Common/EpochExtensions.cs +++ b/src/EventStore.Client.Common/EpochExtensions.cs @@ -1,7 +1,23 @@ namespace EventStore.Client; static class EpochExtensions { +#if NET static readonly DateTime UnixEpoch = DateTime.UnixEpoch; +#else + const long TicksPerMillisecond = 10000; + const long TicksPerSecond = TicksPerMillisecond * 1000; + const long TicksPerMinute = TicksPerSecond * 60; + const long TicksPerHour = TicksPerMinute * 60; + const long TicksPerDay = TicksPerHour * 24; + const int DaysPerYear = 365; + const int DaysPer4Years = DaysPerYear * 4 + 1; + const int DaysPer100Years = DaysPer4Years * 25 - 1; + const int DaysPer400Years = DaysPer100Years * 4 + 1; + const int DaysTo1970 = DaysPer400Years * 4 + DaysPer100Years * 3 + DaysPer4Years * 17 + DaysPerYear; + const long UnixEpochTicks = DaysTo1970 * TicksPerDay; + + static readonly DateTime UnixEpoch = new(UnixEpochTicks, DateTimeKind.Utc); +#endif public static DateTime FromTicksSinceEpoch(this long value) => new(UnixEpoch.Ticks + value, DateTimeKind.Utc); diff --git a/src/EventStore.Client.Common/Shims/Index.cs b/src/EventStore.Client.Common/Shims/Index.cs new file mode 100644 index 000000000..67af4a05d --- /dev/null +++ b/src/EventStore.Client.Common/Shims/Index.cs @@ -0,0 +1,131 @@ +#if !NET +using System.Runtime.CompilerServices; + +namespace System; + +/// Represent a type can be used to index a collection either from the start or the end. +/// +/// Index is used by the C# compiler to support the new index syntax +/// +/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; +/// int lastElement = someArray[^1]; // lastElement = 5 +/// +/// +internal readonly struct Index : IEquatable +{ + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new Index(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new Index(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + return ~_value; + else + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + int offset = _value; + if (IsFromEnd) + { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) => value is Index && _value == ((Index)value)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() => IsFromEnd ? $"^{((uint)Value)}" : ((uint)Value).ToString(); +} +#endif diff --git a/src/EventStore.Client.Common/Shims/IsExternalInit.cs b/src/EventStore.Client.Common/Shims/IsExternalInit.cs new file mode 100644 index 000000000..a77ccc3c3 --- /dev/null +++ b/src/EventStore.Client.Common/Shims/IsExternalInit.cs @@ -0,0 +1,9 @@ +#if !NET +using System.ComponentModel; + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices; + +[EditorBrowsable(EditorBrowsableState.Never)] +internal class IsExternalInit{} +#endif diff --git a/src/EventStore.Client.Common/Shims/Range.cs b/src/EventStore.Client.Common/Shims/Range.cs new file mode 100644 index 000000000..9d4b88f2f --- /dev/null +++ b/src/EventStore.Client.Common/Shims/Range.cs @@ -0,0 +1,79 @@ +#if !NET +using System.Runtime.CompilerServices; + +namespace System; + +/// Represent a range has start and end indexes. +/// +/// Range is used by the C# compiler to support the range syntax. +/// +/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; +/// int[] subArray1 = someArray[0..2]; // { 1, 2 } +/// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } +/// +/// +internal readonly struct Range : IEquatable +{ + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range(Index start, Index end) + { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) => + value is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + + /// Returns the hash code for this instance. + public override int GetHashCode() => Start.GetHashCode() * 31 + End.GetHashCode(); + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() => $"{Start}..{End}"; + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new(start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new(Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new(Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (int Offset, int Length) GetOffsetAndLength(int length) + { + int start = Start.GetOffset(length); + int end = End.GetOffset(length); + + if ((uint)end > (uint)length || (uint)start > (uint)end) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return (start, end - start); + } +} +#endif diff --git a/src/EventStore.Client.Common/Shims/TaskCompletionSource.cs b/src/EventStore.Client.Common/Shims/TaskCompletionSource.cs new file mode 100644 index 000000000..e7e88a97f --- /dev/null +++ b/src/EventStore.Client.Common/Shims/TaskCompletionSource.cs @@ -0,0 +1,8 @@ +#if !NET +namespace System.Threading.Tasks; + +internal class TaskCompletionSource : TaskCompletionSource { + public void SetResult() => base.SetResult(null); + public bool TrySetResult() => base.TrySetResult(null); +} +#endif diff --git a/src/EventStore.Client.Operations/EventStore.Client.Operations.csproj b/src/EventStore.Client.Operations/EventStore.Client.Operations.csproj index 2a351190d..37299e1a1 100644 --- a/src/EventStore.Client.Operations/EventStore.Client.Operations.csproj +++ b/src/EventStore.Client.Operations/EventStore.Client.Operations.csproj @@ -1,6 +1,6 @@  - - The GRPC client API for Event Store Operations, e.g., Scavenging. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - + + The GRPC client API for Event Store Operations, e.g., Scavenging. Get the open source or commercial versions of Event Store server from https://eventstore.com/ + diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStore.Client.PersistentSubscriptions.csproj b/src/EventStore.Client.PersistentSubscriptions/EventStore.Client.PersistentSubscriptions.csproj index 855b8780f..cb7e38ce1 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStore.Client.PersistentSubscriptions.csproj +++ b/src/EventStore.Client.PersistentSubscriptions/EventStore.Client.PersistentSubscriptions.csproj @@ -1,6 +1,9 @@  - - The GRPC client API for Event Store Persistent Subscriptions. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - + + The GRPC client API for Event Store Persistent Subscriptions. Get the open source or commercial versions of Event Store server from https://eventstore.com/ + + + + diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs b/src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs index 0047e1c73..56b8d305d 100644 --- a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs +++ b/src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs @@ -13,11 +13,11 @@ namespace EventStore.Client { /// public class PersistentSubscription : IDisposable { private readonly Func _eventAppeared; - private readonly Action _subscriptionDropped; - private readonly IDisposable _disposable; - private readonly CancellationToken _cancellationToken; - private readonly AsyncDuplexStreamingCall _call; - private readonly ILogger _log; + private readonly Action _subscriptionDropped; + private readonly IDisposable _disposable; + private readonly CancellationToken _cancellationToken; + private readonly AsyncDuplexStreamingCall _call; + private readonly ILogger _log; private int _subscriptionDroppedInvoked; @@ -60,13 +60,13 @@ private PersistentSubscription( Action subscriptionDropped, CancellationToken cancellationToken, IDisposable disposable) { - _call = call; - _eventAppeared = eventAppeared; + _call = call; + _eventAppeared = eventAppeared; _subscriptionDropped = subscriptionDropped; - _cancellationToken = cancellationToken; - _disposable = disposable; - _log = log; - SubscriptionId = call.ResponseStream.Current.SubscriptionConfirmation.SubscriptionId; + _cancellationToken = cancellationToken; + _disposable = disposable; + _log = log; + SubscriptionId = call.ResponseStream.Current.SubscriptionConfirmation.SubscriptionId; Task.Run(Subscribe); } @@ -130,7 +130,7 @@ public Task Nack(PersistentSubscriptionNakEventAction action, string reason, par /// The s to nak. There should not be more than 2000 to nak at a time. /// The number of resolvedEvents exceeded the limit of 2000. public Task Nack(PersistentSubscriptionNakEventAction action, string reason, - params ResolvedEvent[] resolvedEvents) => + params ResolvedEvent[] resolvedEvents) => Nack(action, reason, Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId)); @@ -186,6 +186,23 @@ await _eventAppeared( } } catch (Exception ex) { if (_subscriptionDroppedInvoked == 0) { +#if NET48 + switch (ex) { + // The gRPC client for .NET 48 uses WinHttpHandler under the hood for sending HTTP requests. + // In certain scenarios, this can lead to exceptions of type WinHttpException being thrown. + // One such scenario is when the server abruptly closes the connection, which results in a WinHttpException with the error code 12030. + // Additionally, there are cases where the server response does not include the 'grpc-status' header. + // The absence of this header leads to an RpcException with the status code 'Cancelled' and the message "No grpc-status found on response". + // The switch statement below handles these specific exceptions and translates them into the appropriate + // PersistentSubscriptionDroppedByServerException exception. The downside of this approach is that it does not return the stream name + // and group name. + case RpcException { StatusCode: StatusCode.Unavailable } rex1 when rex1.Status.Detail.Contains("WinHttpException: Error 12030"): + case RpcException { StatusCode: StatusCode.Cancelled } rex2 + when rex2.Status.Detail.Contains("No grpc-status found on response"): + ex = new PersistentSubscriptionDroppedByServerException("", "", ex); + break; + } +#endif _log.LogError(ex, "Persistent Subscription {subscriptionId} was dropped because an error occurred on the server.", SubscriptionId); @@ -205,7 +222,7 @@ await _eventAppeared( ConvertToEventRecord(response.Event.Link), response.Event.PositionCase switch { ReadResp.Types.ReadEvent.PositionOneofCase.CommitPosition => response.Event.CommitPosition, - _ => null + _ => null }); EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent? e) => @@ -250,14 +267,14 @@ private Task NackInternal(Uuid[] ids, PersistentSubscriptionNakEventAction actio Array.ConvertAll(ids, id => id.ToDto()) }, Action = action switch { - PersistentSubscriptionNakEventAction.Park => ReadReq.Types.Nack.Types.Action.Park, + PersistentSubscriptionNakEventAction.Park => ReadReq.Types.Nack.Types.Action.Park, PersistentSubscriptionNakEventAction.Retry => ReadReq.Types.Nack.Types.Action.Retry, - PersistentSubscriptionNakEventAction.Skip => ReadReq.Types.Nack.Types.Action.Skip, - PersistentSubscriptionNakEventAction.Stop => ReadReq.Types.Nack.Types.Action.Stop, - _ => ReadReq.Types.Nack.Types.Action.Unknown + PersistentSubscriptionNakEventAction.Skip => ReadReq.Types.Nack.Types.Action.Skip, + PersistentSubscriptionNakEventAction.Stop => ReadReq.Types.Nack.Types.Action.Stop, + _ => ReadReq.Types.Nack.Types.Action.Unknown }, Reason = reason } }); } -} +} \ No newline at end of file diff --git a/src/EventStore.Client.ProjectionManagement/EventStore.Client.ProjectionManagement.csproj b/src/EventStore.Client.ProjectionManagement/EventStore.Client.ProjectionManagement.csproj index 63ce0af0d..678656cff 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStore.Client.ProjectionManagement.csproj +++ b/src/EventStore.Client.ProjectionManagement/EventStore.Client.ProjectionManagement.csproj @@ -1,6 +1,6 @@  - - The GRPC client API for managing Event Store Projections. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - + + The GRPC client API for managing Event Store Projections. Get the open source or commercial versions of Event Store server from https://eventstore.com/ + diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs index ec23295a5..64187fe1f 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs @@ -26,7 +26,11 @@ public async Task GetResultAsync(string name, string? partition = var value = await GetResultInternalAsync(name, partition, deadline, userCredentials, cancellationToken) .ConfigureAwait(false); +#if NET await using var stream = new MemoryStream(); +#else + using var stream = new MemoryStream(); +#endif await using var writer = new Utf8JsonWriter(stream); var serializer = new ValueSerializer(); serializer.Write(writer, value, DefaultJsonSerializerOptions); @@ -53,7 +57,11 @@ public async Task GetResultAsync(string name, string? partition = null, CancellationToken cancellationToken = default) { var value = await GetResultInternalAsync(name, partition, deadline, userCredentials, cancellationToken) .ConfigureAwait(false); +#if NET await using var stream = new MemoryStream(); +#else + using var stream = new MemoryStream(); +#endif await using var writer = new Utf8JsonWriter(stream); var serializer = new ValueSerializer(); serializer.Write(writer, value, DefaultJsonSerializerOptions); @@ -93,7 +101,11 @@ public async Task GetStateAsync(string name, string? partition = n var value = await GetStateInternalAsync(name, partition, deadline, userCredentials, cancellationToken) .ConfigureAwait(false); +#if NET await using var stream = new MemoryStream(); +#else + using var stream = new MemoryStream(); +#endif await using var writer = new Utf8JsonWriter(stream); var serializer = new ValueSerializer(); serializer.Write(writer, value, DefaultJsonSerializerOptions); @@ -120,7 +132,11 @@ public async Task GetStateAsync(string name, string? partition = null, var value = await GetStateInternalAsync(name, partition, deadline, userCredentials, cancellationToken) .ConfigureAwait(false); +#if NET await using var stream = new MemoryStream(); +#else + using var stream = new MemoryStream(); +#endif await using var writer = new Utf8JsonWriter(stream); var serializer = new ValueSerializer(); serializer.Write(writer, value, DefaultJsonSerializerOptions); @@ -175,9 +191,9 @@ public override void Write(Utf8JsonWriter writer, Value value, JsonSerializerOpt break; case Value.KindOneofCase.StructValue: writer.WriteStartObject(); - foreach (var (name, item) in value.StructValue.Fields) { - writer.WritePropertyName(name); - Write(writer, item, options); + foreach (var map in value.StructValue.Fields) { + writer.WritePropertyName(map.Key); + Write(writer, map.Value, options); } writer.WriteEndObject(); diff --git a/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj b/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj index 430821e42..a9c2969f2 100644 --- a/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj +++ b/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj @@ -1,6 +1,12 @@  - - The GRPC client API for Event Store Streams. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - + + The GRPC client API for Event Store Streams. Get the open source or commercial versions of Event Store server from https://eventstore.com/ + + + + + + + diff --git a/src/EventStore.Client.Streams/EventStoreClient.Append.cs b/src/EventStore.Client.Streams/EventStoreClient.Append.cs index a780f1e00..6cb2c9fd5 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Append.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Append.cs @@ -8,6 +8,8 @@ using EventStore.Client.Streams; using Grpc.Core; using Microsoft.Extensions.Logging; +using System.Runtime.CompilerServices; + namespace EventStore.Client { public partial class EventStoreClient { /// @@ -43,7 +45,7 @@ public async Task AppendToStreamAsync( new AppendReq { Options = new AppendReq.Types.Options { StreamIdentifier = streamName, - Revision = expectedRevision + Revision = expectedRevision } }, eventData, options, deadline, userCredentials, cancellationToken); @@ -111,8 +113,8 @@ private async ValueTask AppendToStreamInternal( header.Options.StreamIdentifier, e.EventId, e.Type); await call.RequestStream.WriteAsync(new AppendReq { ProposedMessage = new AppendReq.Types.ProposedMessage { - Id = e.EventId.ToDto(), - Data = ByteString.CopyFrom(e.Data.Span), + Id = e.EventId.ToDto(), + Data = ByteString.CopyFrom(e.Data.Span), CustomMetadata = ByteString.CopyFrom(e.Metadata.Span), Metadata = { {Constants.Metadata.Type, e.Type}, @@ -140,10 +142,10 @@ await call.RequestStream.WriteAsync(new AppendReq { } else { if (response.WrongExpectedVersion != null) { var actualStreamRevision = response.WrongExpectedVersion.CurrentRevisionOptionCase switch { - AppendResp.Types.WrongExpectedVersion.CurrentRevisionOptionOneofCase.CurrentNoStream => - StreamRevision.None, - _ => new StreamRevision(response.WrongExpectedVersion.CurrentRevision) - }; + AppendResp.Types.WrongExpectedVersion.CurrentRevisionOptionOneofCase.CurrentNoStream => + StreamRevision.None, + _ => new StreamRevision(response.WrongExpectedVersion.CurrentRevision) + }; _log.LogDebug( "Append to stream failed with Wrong Expected Version - {streamName}/{expectedRevision}/{currentRevision}", @@ -152,7 +154,7 @@ await call.RequestStream.WriteAsync(new AppendReq { if (operationOptions.ThrowOnAppendFailure) { if (response.WrongExpectedVersion.ExpectedRevisionOptionCase == AppendResp.Types - .WrongExpectedVersion.ExpectedRevisionOptionOneofCase.ExpectedRevision) { + .WrongExpectedVersion.ExpectedRevisionOptionOneofCase.ExpectedRevision) { throw new WrongExpectedVersionException(header.Options.StreamIdentifier!, new StreamRevision(response.WrongExpectedVersion.ExpectedRevision), actualStreamRevision); @@ -173,7 +175,7 @@ await call.RequestStream.WriteAsync(new AppendReq { } if (response.WrongExpectedVersion.ExpectedRevisionOptionCase == AppendResp.Types - .WrongExpectedVersion.ExpectedRevisionOptionOneofCase.ExpectedRevision) { + .WrongExpectedVersion.ExpectedRevisionOptionOneofCase.ExpectedRevision) { writeResult = new WrongExpectedVersionResult(header.Options.StreamIdentifier!, new StreamRevision(response.WrongExpectedVersion.ExpectedRevision), actualStreamRevision); @@ -194,34 +196,34 @@ await call.RequestStream.WriteAsync(new AppendReq { private class StreamAppender : IDisposable { - private readonly EventStoreClientSettings _settings; - private readonly CancellationToken _cancellationToken; - private readonly Action _onException; - private readonly Channel _channel; + private readonly EventStoreClientSettings _settings; + private readonly CancellationToken _cancellationToken; + private readonly Action _onException; + private readonly Channel _channel; private readonly ConcurrentDictionary> _pendingRequests; private readonly Task?> _callTask; public StreamAppender(EventStoreClientSettings settings, - Task?> callTask, CancellationToken cancellationToken, - Action onException) { - _settings = settings; - _callTask = callTask; + Task?> callTask, CancellationToken cancellationToken, + Action onException) { + _settings = settings; + _callTask = callTask; _cancellationToken = cancellationToken; - _onException = onException; - _channel = System.Threading.Channels.Channel.CreateBounded(10000); - _pendingRequests = new ConcurrentDictionary>(); - _ = Task.Factory.StartNew(Send); - _ = Task.Factory.StartNew(Receive); + _onException = onException; + _channel = System.Threading.Channels.Channel.CreateBounded(10000); + _pendingRequests = new ConcurrentDictionary>(); + _ = Task.Factory.StartNew(Send); + _ = Task.Factory.StartNew(Receive); } public ValueTask Append(string streamName, StreamRevision expectedStreamPosition, - IEnumerable events, TimeSpan? timeoutAfter, CancellationToken cancellationToken = default) => + IEnumerable events, TimeSpan? timeoutAfter, CancellationToken cancellationToken = default) => AppendInternal(BatchAppendReq.Types.Options.Create(streamName, expectedStreamPosition, timeoutAfter), events, cancellationToken); public ValueTask Append(string streamName, StreamState expectedStreamState, - IEnumerable events, TimeSpan? timeoutAfter, CancellationToken cancellationToken = default) => + IEnumerable events, TimeSpan? timeoutAfter, CancellationToken cancellationToken = default) => AppendInternal(BatchAppendReq.Types.Options.Create(streamName, expectedStreamState, timeoutAfter), events, cancellationToken); @@ -240,7 +242,7 @@ private async Task Receive() { } await foreach (var response in call.ResponseStream.ReadAllAsync(_cancellationToken) - .ConfigureAwait(false)) { + .ConfigureAwait(false)) { if (!_pendingRequests.TryRemove(Uuid.FromDto(response.CorrelationId), out var writeResult)) { continue; // TODO: Log? } @@ -257,8 +259,8 @@ private async Task Receive() { // complete whatever tcs's we have _onException(ex); - foreach (var (_, source) in _pendingRequests) { - source.TrySetException(ex); + foreach (var request in _pendingRequests) { + request.Value.TrySetException(ex); } } } @@ -268,8 +270,8 @@ private async Task Send() { if (call is null) throw new NotSupportedException("Server does not support batch append"); - await foreach (var appendRequest in _channel.Reader.ReadAllAsync(_cancellationToken) - .ConfigureAwait(false)) { + await foreach (var appendRequest in ReadAllAsync(_channel.Reader, _cancellationToken) + .ConfigureAwait(false)) { await call.RequestStream.WriteAsync(appendRequest).ConfigureAwait(false); } @@ -277,9 +279,9 @@ private async Task Send() { } private async ValueTask AppendInternal(BatchAppendReq.Types.Options options, - IEnumerable events, CancellationToken cancellationToken) { - var batchSize = 0; - var correlationId = Uuid.NewUuid(); + IEnumerable events, CancellationToken cancellationToken) { + var batchSize = 0; + var correlationId = Uuid.NewUuid(); var correlationIdDto = correlationId.ToDto(); var complete = _pendingRequests.GetOrAdd(correlationId, new TaskCompletionSource()); @@ -296,13 +298,13 @@ private async ValueTask AppendInternal(BatchAppendReq.Types.Option return await complete.Task.ConfigureAwait(false); IEnumerable GetRequests() { - bool first = true; - var proposedMessages = new List(); + bool first = true; + var proposedMessages = new List(); foreach (var @event in events) { var proposedMessage = new BatchAppendReq.Types.ProposedMessage { - Data = ByteString.CopyFrom(@event.Data.Span), + Data = ByteString.CopyFrom(@event.Data.Span), CustomMetadata = ByteString.CopyFrom(@event.Metadata.Span), - Id = @event.EventId.ToDto(), + Id = @event.EventId.ToDto(), Metadata = { {Constants.Metadata.Type, @event.Type}, {Constants.Metadata.ContentType, @event.ContentType} @@ -318,8 +320,8 @@ IEnumerable GetRequests() { yield return new BatchAppendReq { ProposedMessages = {proposedMessages}, - CorrelationId = correlationIdDto, - Options = first ? options : null + CorrelationId = correlationIdDto, + Options = first ? options : null }; first = false; proposedMessages.Clear(); @@ -328,9 +330,9 @@ IEnumerable GetRequests() { yield return new BatchAppendReq { ProposedMessages = {proposedMessages}, - IsFinal = true, - CorrelationId = correlationIdDto, - Options = first ? options : null + IsFinal = true, + CorrelationId = correlationIdDto, + Options = first ? options : null }; } } @@ -339,5 +341,18 @@ public void Dispose() { _channel.Writer.TryComplete(); } } + + private static async IAsyncEnumerable ReadAllAsync(ChannelReader reader, [EnumeratorCancellation] CancellationToken cancellationToken = default) { +#if NET + await foreach (var item in reader.ReadAllAsync(cancellationToken)) + yield return item; +#else + while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) { + while (reader.TryRead(out T? item)) { + yield return item; + } + } +#endif + } } -} +} \ No newline at end of file diff --git a/src/EventStore.Client.Streams/EventStoreClient.Read.cs b/src/EventStore.Client.Streams/EventStoreClient.Read.cs index 25635f26a..39cc588d9 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Read.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Read.cs @@ -87,7 +87,7 @@ async IAsyncEnumerable GetMessages() { } try { - await foreach (var message in _channel.Reader.ReadAllAsync().ConfigureAwait(false)) { + await foreach (var message in ReadAllAsync(_channel.Reader).ConfigureAwait(false)) { if (message is StreamMessage.LastAllStreamPosition(var position)) { LastPosition = position; } @@ -151,7 +151,7 @@ public async IAsyncEnumerator GetAsyncEnumerator( CancellationToken cancellationToken = default) { try { - await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { + await foreach (var message in ReadAllAsync(_channel.Reader, cancellationToken).ConfigureAwait(false)) { if (message is not StreamMessage.Event e) { continue; } @@ -250,7 +250,7 @@ async IAsyncEnumerable GetMessages() { } try { - await foreach (var message in _channel.Reader.ReadAllAsync().ConfigureAwait(false)) { + await foreach (var message in ReadAllAsync(_channel.Reader).ConfigureAwait(false)) { switch (message) { case StreamMessage.FirstStreamPosition(var streamPosition): FirstStreamPosition = streamPosition; @@ -348,7 +348,7 @@ public async IAsyncEnumerator GetAsyncEnumerator( CancellationToken cancellationToken = default) { try { - await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { + await foreach (var message in ReadAllAsync(_channel.Reader, cancellationToken).ConfigureAwait(false)) { if (message is StreamMessage.NotFound) { throw new StreamNotFoundException(StreamName); } diff --git a/src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj b/src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj index 68b8d5098..2c9308e2e 100644 --- a/src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj +++ b/src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj @@ -1,9 +1,9 @@  - - The GRPC client API for managing users in Event Store. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - - - - + + The GRPC client API for managing users in Event Store. Get the open source or commercial versions of Event Store server from https://eventstore.com/ + + + + diff --git a/src/EventStore.Client.UserManagement/EventStoreUserManagementClientCollectionExtensions.cs b/src/EventStore.Client.UserManagement/EventStoreUserManagementClientCollectionExtensions.cs index 539f5849f..2b25f816c 100644 --- a/src/EventStore.Client.UserManagement/EventStoreUserManagementClientCollectionExtensions.cs +++ b/src/EventStore.Client.UserManagement/EventStoreUserManagementClientCollectionExtensions.cs @@ -1,5 +1,6 @@ // ReSharper disable CheckNamespace +using System.Net.Http; using EventStore.Client; using Grpc.Core.Interceptors; using Microsoft.Extensions.DependencyInjection.Extensions; diff --git a/src/EventStore.Client/ChannelFactory.cs b/src/EventStore.Client/ChannelFactory.cs index 06d2888de..951aaf055 100644 --- a/src/EventStore.Client/ChannelFactory.cs +++ b/src/EventStore.Client/ChannelFactory.cs @@ -1,7 +1,7 @@ -using System; -using EndPoint = System.Net.EndPoint; using System.Net.Http; using Grpc.Net.Client; +using System.Net.Security; +using EndPoint = System.Net.EndPoint; using TChannel = Grpc.Net.Client.GrpcChannel; namespace EventStore.Client { @@ -17,26 +17,47 @@ public static TChannel CreateChannel(EventStoreClientSettings settings, EndPoint } return TChannel.ForAddress(address, new GrpcChannelOptions { +#if NET HttpClient = new HttpClient(CreateHandler(), true) { Timeout = System.Threading.Timeout.InfiniteTimeSpan, - DefaultRequestVersion = new Version(2, 0), + DefaultRequestVersion = new Version(2, 0) }, - LoggerFactory = settings.LoggerFactory, - Credentials = settings.ChannelCredentials, - DisposeHttpClient = true, +#else + HttpHandler = CreateHandler(), +#endif + LoggerFactory = settings.LoggerFactory, + Credentials = settings.ChannelCredentials, + DisposeHttpClient = true, MaxReceiveMessageSize = MaxReceiveMessageLength }); - + HttpMessageHandler CreateHandler() { if (settings.CreateHttpMessageHandler != null) { return settings.CreateHttpMessageHandler.Invoke(); } +#if NET + var handler = new SocketsHttpHandler { + KeepAlivePingDelay = settings.ConnectivitySettings.KeepAliveInterval, + KeepAlivePingTimeout = settings.ConnectivitySettings.KeepAliveTimeout, + EnableMultipleHttp2Connections = true, + }; - return new SocketsHttpHandler { - KeepAlivePingDelay = settings.ConnectivitySettings.KeepAliveInterval, - KeepAlivePingTimeout = settings.ConnectivitySettings.KeepAliveTimeout, + if (!settings.ConnectivitySettings.TlsVerifyCert) { + handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; + } +#else + var handler = new WinHttpHandler { + TcpKeepAliveEnabled = true, + TcpKeepAliveTime = settings.ConnectivitySettings.KeepAliveTimeout, + TcpKeepAliveInterval = settings.ConnectivitySettings.KeepAliveInterval, EnableMultipleHttp2Connections = true }; + + if (!settings.ConnectivitySettings.TlsVerifyCert) { + handler.ServerCertificateValidationCallback = delegate { return true; }; + } +#endif + return handler; } } } diff --git a/src/EventStore.Client/EventStore.Client.csproj b/src/EventStore.Client/EventStore.Client.csproj index 92a14142f..3ab3a3faf 100644 --- a/src/EventStore.Client/EventStore.Client.csproj +++ b/src/EventStore.Client/EventStore.Client.csproj @@ -1,24 +1,33 @@  - - EventStore.Client - The base GRPC client library for Event Store. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - EventStore.Client.Grpc - - - - - - - - - - - - - - - - - + + EventStore.Client + The base GRPC client library for Event Store. Get the open source or commercial versions of Event Store server from https://eventstore.com/ + EventStore.Client.Grpc + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs index 76b3c9c6f..37d9da65b 100644 --- a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs +++ b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs @@ -105,30 +105,35 @@ public static EventStoreClientSettings Parse(string connectionString) { return CreateSettings(scheme, userInfo, hosts, options); } - private static EventStoreClientSettings CreateSettings(string scheme, (string user, string pass)? userInfo, - EndPoint[] hosts, Dictionary options) { + private static EventStoreClientSettings CreateSettings( + string scheme, (string user, string pass)? userInfo, + EndPoint[] hosts, Dictionary options + ) { var settings = new EventStoreClientSettings { ConnectivitySettings = EventStoreClientConnectivitySettings.Default, - OperationOptions = EventStoreClientOperationOptions.Default + OperationOptions = EventStoreClientOperationOptions.Default }; if (userInfo.HasValue) settings.DefaultCredentials = new UserCredentials(userInfo.Value.user, userInfo.Value.pass); var typedOptions = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - foreach (var (key, value) in options) { - if (!SettingsType.TryGetValue(key, out var type)) - throw new InvalidSettingException($"Unknown option: {key}"); + foreach (var kv in options) { + if (!SettingsType.TryGetValue(kv.Key, out var type)) + throw new InvalidSettingException($"Unknown option: {kv.Key}"); + if (type == typeof(int)) { - if (!int.TryParse(value, out var intValue)) - throw new InvalidSettingException($"{key} must be an integer value"); - typedOptions.Add(key, intValue); + if (!int.TryParse(kv.Value, out var intValue)) + throw new InvalidSettingException($"{kv.Key} must be an integer value"); + + typedOptions.Add(kv.Key, intValue); } else if (type == typeof(bool)) { - if (!bool.TryParse(value, out var boolValue)) - throw new InvalidSettingException($"{key} must be either true or false"); - typedOptions.Add(key, boolValue); + if (!bool.TryParse(kv.Value, out var boolValue)) + throw new InvalidSettingException($"{kv.Key} must be either true or false"); + + typedOptions.Add(kv.Key, boolValue); } else if (type == typeof(string)) { - typedOptions.Add(key, value); + typedOptions.Add(kv.Key, kv.Value); } } @@ -198,8 +203,13 @@ private static EventStoreClientSettings CreateSettings(string scheme, (string us if (typedOptions.TryGetValue(TlsVerifyCert, out var tlsVerifyCert)) { settings.ConnectivitySettings.TlsVerifyCert = (bool)tlsVerifyCert; } - - settings.CreateHttpMessageHandler = () => { + + settings.CreateHttpMessageHandler = CreateDefaultHandler; + + return settings; + + HttpMessageHandler CreateDefaultHandler() { +#if NET var handler = new SocketsHttpHandler { KeepAlivePingDelay = settings.ConnectivitySettings.KeepAliveInterval, KeepAlivePingTimeout = settings.ConnectivitySettings.KeepAliveTimeout, @@ -209,11 +219,20 @@ private static EventStoreClientSettings CreateSettings(string scheme, (string us if (!settings.ConnectivitySettings.TlsVerifyCert) { handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; } +#else + var handler = new WinHttpHandler { + TcpKeepAliveEnabled = true, + TcpKeepAliveTime = settings.ConnectivitySettings.KeepAliveTimeout, + TcpKeepAliveInterval = settings.ConnectivitySettings.KeepAliveInterval, + EnableMultipleHttp2Connections = true + }; + if (!settings.ConnectivitySettings.TlsVerifyCert) { + handler.ServerCertificateValidationCallback = delegate { return true; }; + } +#endif return handler; - }; - - return settings; + } } private static string ParseScheme(string s) => diff --git a/src/EventStore.Client/HttpFallback.cs b/src/EventStore.Client/HttpFallback.cs index 2a734a1c0..c1b213591 100644 --- a/src/EventStore.Client/HttpFallback.cs +++ b/src/EventStore.Client/HttpFallback.cs @@ -39,7 +39,11 @@ internal async Task HttpGetAsync(string path, ChannelInfo channelInfo, Tim var httpResult = await HttpSendAsync(request, onNotFound, deadline, cancellationToken).ConfigureAwait(false); +#if NET var json = await httpResult.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); +#else + var json = await httpResult.Content.ReadAsStringAsync().ConfigureAwait(false); +#endif var result = JsonSerializer.Deserialize(json, _jsonSettings); if (result == null) { diff --git a/src/EventStore.Client/Interceptors/TypedExceptionInterceptor.cs b/src/EventStore.Client/Interceptors/TypedExceptionInterceptor.cs index 75754f95f..d24dd2a8e 100644 --- a/src/EventStore.Client/Interceptors/TypedExceptionInterceptor.cs +++ b/src/EventStore.Client/Interceptors/TypedExceptionInterceptor.cs @@ -1,4 +1,3 @@ - using System.Diagnostics.CodeAnalysis; using Grpc.Core; using Grpc.Core.Interceptors; @@ -14,8 +13,11 @@ class TypedExceptionInterceptor : Interceptor { }; public TypedExceptionInterceptor(Dictionary> customExceptionMap) { +#if NET48 + var map = new Dictionary>(DefaultExceptionMap.Concat(customExceptionMap).ToDictionary(x => x.Key, x => x.Value)); +#else var map = new Dictionary>(DefaultExceptionMap.Concat(customExceptionMap)); - +#endif ConvertRpcException = rpcEx => { if (rpcEx.TryMapException(map, out var ex)) throw ex; @@ -117,13 +119,13 @@ public static NotAuthenticatedException ToNotAuthenticatedException(this RpcExce public static RpcException ToDeadlineExceededRpcException(this RpcException exception) => new(new Status(DeadlineExceeded, exception.Status.Detail, exception.Status.DebugException)); - public static bool TryMapException(this RpcException exception, Dictionary> map, [MaybeNullWhen(false)] out Exception createdException) { + public static bool TryMapException(this RpcException exception, Dictionary> map, out Exception createdException) { if (exception.Trailers.TryGetValue(Exceptions.ExceptionKey, out var key) && map.TryGetValue(key!, out var factory)) { createdException = factory.Invoke(exception); return true; } - createdException = null; + createdException = null!; return false; } } diff --git a/src/EventStore.Client/Position.cs b/src/EventStore.Client/Position.cs index 71fb54ef8..169439804 100644 --- a/src/EventStore.Client/Position.cs +++ b/src/EventStore.Client/Position.cs @@ -164,7 +164,7 @@ public bool Equals(Position other) => /// true if the value was converted successfully; otherwise, false. public static bool TryParse(string value, out Position? position) { position = null; - var parts = value.Split("/"); + var parts = value.Split('/'); if (parts.Length != 2) { return false; @@ -184,7 +184,7 @@ public static bool TryParse(string value, out Position? position) { static bool TryParsePosition(string expectedPrefix, string v, out ulong p) { p = 0; - var prts = v.Split(":"); + var prts = v.Split(':'); if (prts.Length != 2) { return false; } diff --git a/src/EventStore.Client/StreamIdentifier.cs b/src/EventStore.Client/StreamIdentifier.cs index fd0ed8d4f..73e0ee25e 100644 --- a/src/EventStore.Client/StreamIdentifier.cs +++ b/src/EventStore.Client/StreamIdentifier.cs @@ -12,7 +12,11 @@ public partial class StreamIdentifier { } if (source._cached != null || source.StreamName.IsEmpty) return source._cached; +#if NET var tmp = Encoding.UTF8.GetString(source.StreamName.Span); +#else + var tmp = Encoding.UTF8.GetString(source.StreamName.ToByteArray()); +#endif //this doesn't have to be thread safe, its just a cache in case the identifier is turned into a string several times source._cached = tmp; return source._cached; diff --git a/src/EventStore.Client/Uuid.cs b/src/EventStore.Client/Uuid.cs index 98b6f9ff6..6c96b1e2d 100644 --- a/src/EventStore.Client/Uuid.cs +++ b/src/EventStore.Client/Uuid.cs @@ -1,4 +1,6 @@ using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace EventStore.Client { /// @@ -66,18 +68,28 @@ private Uuid(Guid value) { Span data = stackalloc byte[16]; - if (!value.TryWriteBytes(data)) { + if (!TryWriteGuidBytes(value, data)) { throw new InvalidOperationException(); } - data.Slice(0, 8).Reverse(); - data.Slice(0, 2).Reverse(); + data[..8].Reverse(); + data[..2].Reverse(); data.Slice(2, 2).Reverse(); data.Slice(4, 4).Reverse(); - data.Slice(8).Reverse(); + data[8..].Reverse(); - _msb = BitConverter.ToInt64(data); - _lsb = BitConverter.ToInt64(data.Slice(8)); + _msb = BitConverterToInt64(data); + _lsb = BitConverterToInt64(data[8..]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long BitConverterToInt64(ReadOnlySpan value) + { +#if NET + return BitConverter.ToInt64(value); +#else + return Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(value)); +#endif } private Uuid(string value) : this(value == null @@ -98,7 +110,7 @@ public UUID ToDto() => new UUID { Structured = new UUID.Types.Structured { LeastSignificantBits = _lsb, - MostSignificantBits = _msb + MostSignificantBits = _msb } }; @@ -148,18 +160,44 @@ public Guid ToGuid() { } Span data = stackalloc byte[16]; - if (!BitConverter.TryWriteBytes(data, _msb) || - !BitConverter.TryWriteBytes(data.Slice(8), _lsb)) { + if (!TryWriteBytes(data, _msb) || + !TryWriteBytes(data[8..], _lsb)) { throw new InvalidOperationException(); } - data.Slice(0, 8).Reverse(); - data.Slice(0, 4).Reverse(); + data[..8].Reverse(); + data[..4].Reverse(); data.Slice(4, 2).Reverse(); data.Slice(6, 2).Reverse(); - data.Slice(8).Reverse(); + data[8..].Reverse(); +#if NET return new Guid(data); +#else + return new Guid(data.ToArray()); +#endif + } + private static bool TryWriteBytes(Span destination, long value) + { + if (destination.Length < sizeof(long)) + return false; + + Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(destination), value); + return true; + } + + private bool TryWriteGuidBytes(Guid value, Span destination) + { +#if NET + return value.TryWriteBytes(destination); +#else + if (destination.Length < 16) + return false; + + var bytes = value.ToByteArray(); + bytes.CopyTo(destination); + return true; +#endif } } } diff --git a/test/Directory.Build.props b/test/Directory.Build.props index a4de0b3a7..0357513c2 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -1,5 +1,5 @@ - + true @@ -7,13 +7,13 @@ - - - + + + - - - + + + all runtime; build; native; contentfiles; analyzers @@ -21,12 +21,20 @@ - - - - - - - + + + + + + + + + + + + + + + diff --git a/test/EventStore.Client.Operations.Tests/EventStore.Client.Operations.Tests.csproj b/test/EventStore.Client.Operations.Tests/EventStore.Client.Operations.Tests.csproj index 1febb6cfb..d43bdceb5 100644 --- a/test/EventStore.Client.Operations.Tests/EventStore.Client.Operations.Tests.csproj +++ b/test/EventStore.Client.Operations.Tests/EventStore.Client.Operations.Tests.csproj @@ -1,6 +1,6 @@ - + diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj b/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj index cd15c30d9..04cf6634b 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj @@ -1,9 +1,9 @@  - + - + diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs index 4f7f2646d..4403087e6 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs @@ -11,8 +11,11 @@ public async Task the_subscription_is_dropped() { var (reason, exception) = await _fixture.Dropped.WithTimeout(); Assert.Equal(SubscriptionDroppedReason.ServerError, reason); var ex = Assert.IsType(exception); + +#if NET Assert.Equal(SystemStreams.AllStream, ex.StreamName); Assert.Equal("groupname123", ex.GroupName); +#endif } [Fact(Skip = "Isn't this how it should work?")] diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs index ce1b19108..ca3afffd6 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs @@ -164,12 +164,13 @@ await Client.SubscribeToAllAsync( (s, e, r, ct) => { counter++; - if (counter == 1) - s.Nack(PersistentSubscriptionNakEventAction.Park, "Test", e); - - if (counter > 10) - tcs.TrySetResult(); - + switch (counter) { + case 1: s.Nack(PersistentSubscriptionNakEventAction.Park, "Test", e); + break; + case > 10: + tcs.TrySetResult(); + break; + } return Task.CompletedTask; }, userCredentials: TestCredentials.Root @@ -178,4 +179,4 @@ await Client.SubscribeToAllAsync( await tcs.Task; } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers.cs index 6113954c0..82c0ec320 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers.cs @@ -13,8 +13,11 @@ public async Task existing_subscriptions_are_dropped() { var (reason, exception) = await _fixture.Dropped.WithTimeout(TimeSpan.FromSeconds(10)); Assert.Equal(SubscriptionDroppedReason.ServerError, reason); var ex = Assert.IsType(exception); + +#if NET Assert.Equal(SystemStreams.AllStream, ex.StreamName); Assert.Equal(Group, ex.GroupName); +#endif } public class Fixture : EventStoreClientFixture { diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber.cs index 8084881da..cb268b603 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber.cs @@ -12,8 +12,11 @@ public async Task the_subscription_is_dropped() { var (reason, exception) = await _fixture.Dropped.WithTimeout(); Assert.Equal(SubscriptionDroppedReason.ServerError, reason); var ex = Assert.IsType(exception); + +#if NET Assert.Equal(Stream, ex.StreamName); Assert.Equal("groupname123", ex.GroupName); +#endif } [Fact(Skip = "Isn't this how it should work?")] diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers.cs index e0712a27d..72b59b778 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers.cs +++ b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers.cs @@ -13,8 +13,11 @@ public async Task existing_subscriptions_are_dropped() { var (reason, exception) = await _fixture.Dropped.WithTimeout(TimeSpan.FromSeconds(10)); Assert.Equal(SubscriptionDroppedReason.ServerError, reason); var ex = Assert.IsType(exception); + +#if NET Assert.Equal(Stream, ex.StreamName); Assert.Equal(Group, ex.GroupName); +#endif } public class Fixture : EventStoreClientFixture { diff --git a/test/EventStore.Client.ProjectionManagement.Tests/EventStore.Client.ProjectionManagement.Tests.csproj b/test/EventStore.Client.ProjectionManagement.Tests/EventStore.Client.ProjectionManagement.Tests.csproj index dac52c701..31381e212 100644 --- a/test/EventStore.Client.ProjectionManagement.Tests/EventStore.Client.ProjectionManagement.Tests.csproj +++ b/test/EventStore.Client.ProjectionManagement.Tests/EventStore.Client.ProjectionManagement.Tests.csproj @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj b/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj index d32b56a43..caf586457 100644 --- a/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj +++ b/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj @@ -1,9 +1,9 @@  - + - + - \ No newline at end of file + diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/reconnection.cs b/test/EventStore.Client.Streams.Tests/Subscriptions/reconnection.cs index df9162130..db9601d4f 100644 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/reconnection.cs +++ b/test/EventStore.Client.Streams.Tests/Subscriptions/reconnection.cs @@ -6,7 +6,7 @@ namespace EventStore.Client.Streams.Tests.Subscriptions; [Trait("Category", "Subscriptions")] public class @reconnection(ITestOutputHelper output, ReconnectionFixture fixture) : EventStoreTests(output, fixture) { [Theory] - [InlineData(4, 1000, 0, 15000)] + [InlineData(4, 5000, 0, 30000)] public async Task when_the_connection_is_lost(int expectedNumberOfEvents, int reconnectDelayMs, int serviceRestartDelayMs, int testTimeoutMs) { using var cancellator = new CancellationTokenSource().With(x => x.CancelAfter(testTimeoutMs)); diff --git a/test/EventStore.Client.Tests.Common/ApplicationInfo.cs b/test/EventStore.Client.Tests.Common/ApplicationInfo.cs index 8a7a1ec7a..0120c21b4 100644 --- a/test/EventStore.Client.Tests.Common/ApplicationInfo.cs +++ b/test/EventStore.Client.Tests.Common/ApplicationInfo.cs @@ -25,7 +25,7 @@ static Application() { var builder = new ConfigurationBuilder() .AddJsonFile("appsettings.json", true) - .AddJsonFile($"appsettings.{Environment}.json", true) // Accept default naming convention + .AddJsonFile($"appsettings.{Environment}.json", true) // Accept default naming convention .AddJsonFile($"appsettings.{Environment.ToLowerInvariant()}.json", true) // Linux is case sensitive .AddEnvironmentVariables(); @@ -33,7 +33,7 @@ static Application() { WriteLine($"APP: {Environment} configuration loaded " + $"with {Configuration.AsEnumerable().Count()} entries " - + $"from {builder.Sources.Count} sources."); + + $"from {builder.Sources.Count()} sources."); IsDevelopment = IsEnvironment(Environments.Development); IsStaging = IsEnvironment(Environments.Staging); @@ -65,4 +65,4 @@ public static class OperatingSystem { public static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj b/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj index ba1a806f8..f666f3871 100644 --- a/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj +++ b/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj @@ -4,38 +4,40 @@ - - - - - - + + + + + + - + - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + - + - + certs\%(RecursiveDir)/%(FileName)%(Extension) @@ -65,6 +67,6 @@ - + diff --git a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreClientFixtureBase.cs b/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreClientFixtureBase.cs index 2a5076437..522495198 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreClientFixtureBase.cs +++ b/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreClientFixtureBase.cs @@ -14,7 +14,7 @@ namespace EventStore.Client; public abstract class EventStoreClientFixtureBase : IAsyncLifetime { public const string TestEventType = "-"; - const string ConnectionStringSingle = "esdb://admin:changeit@localhost:2113/?tlsVerifyCert=false"; + const string ConnectionStringSingle = "esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=false"; const string ConnectionStringCluster = "esdb://admin:changeit@localhost:2113,localhost:2112,localhost:2111?tls=true&tlsVerifyCert=false"; static readonly Subject LogEventSubject = new(); @@ -88,9 +88,6 @@ static void ConfigureLogging() { .WriteTo.Seq("http://localhost:5341/", period: TimeSpan.FromMilliseconds(1)); Log.Logger = loggerConfiguration.CreateLogger(); -#if GRPC_CORE - GrpcEnvironment.SetLogger(new GrpcCoreSerilogLogger(Log.Logger.ForContext())); -#endif AppDomain.CurrentDomain.DomainUnload += (_, e) => Log.CloseAndFlush(); } @@ -147,4 +144,4 @@ public void CaptureLogs(ITestOutputHelper testOutputHelper) { _disposables.Add(subscription); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServer.cs b/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServer.cs index 772946347..fb7e5f601 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServer.cs +++ b/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServer.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http; using Ductus.FluentDocker.Builders; using Ductus.FluentDocker.Extensions; using Ductus.FluentDocker.Model.Builders; @@ -24,13 +25,19 @@ public EventStoreTestServer( _hostCertificatePath = hostCertificatePath; VerifyCertificatesExist(); - _httpClient = new( - new SocketsHttpHandler { - SslOptions = { RemoteCertificateValidationCallback = delegate { return true; } } - } - ) { - BaseAddress = address +#if NET + _httpClient = new HttpClient(new SocketsHttpHandler { + SslOptions = {RemoteCertificateValidationCallback = delegate { return true; }} + }) { + BaseAddress = address, }; +#else + _httpClient = new HttpClient(new WinHttpHandler { + ServerCertificateValidationCallback = delegate { return true; } + }) { + BaseAddress = address, + }; +#endif var env = new Dictionary { ["EVENTSTORE_DB_LOG_FORMAT"] = "V2", @@ -46,8 +53,8 @@ public EventStoreTestServer( ["EVENTSTORE_DISABLE_LOG_FILE"] = "true" }; - foreach (var (key, value) in envOverrides ?? Enumerable.Empty>()) - env[key] = value; + foreach (var val in envOverrides ?? Enumerable.Empty>()) + env[val.Key] = val.Value; _eventStore = new Builder() .UseContainer() @@ -89,7 +96,7 @@ public ValueTask DisposeAsync() { _httpClient?.Dispose(); _eventStore?.Dispose(); - return ValueTask.CompletedTask; + return new ValueTask(Task.CompletedTask); } static Version GetVersion() { @@ -125,4 +132,4 @@ void VerifyCertificatesExist() { $"Could not locate the certificates file {file} needed to run EventStoreDB. Please run the 'gencert' tool at the root of the repository." ); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerCluster.cs b/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerCluster.cs index e9317c88e..ceb263e15 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerCluster.cs +++ b/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerCluster.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http; using Ductus.FluentDocker.Builders; using Ductus.FluentDocker.Common; using Ductus.FluentDocker.Services; @@ -21,13 +22,19 @@ public EventStoreTestServerCluster( _eventStoreCluster = BuildCluster(envOverrides); - _httpClient = new( - new SocketsHttpHandler { - SslOptions = { RemoteCertificateValidationCallback = delegate { return true; } } - } - ) { - BaseAddress = address +#if NET + _httpClient = new HttpClient(new SocketsHttpHandler { + SslOptions = {RemoteCertificateValidationCallback = delegate { return true; }} + }) { + BaseAddress = address, }; +#else + _httpClient = new HttpClient(new WinHttpHandler { + ServerCertificateValidationCallback = delegate { return true; } + }) { + BaseAddress = address, + }; +#endif } public async Task StartAsync(CancellationToken cancellationToken = default) { @@ -83,4 +90,4 @@ ICompositeService BuildCluster(IDictionary? envOverrides = null) .RemoveOrphans() .Build(); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerExternal.cs b/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerExternal.cs index 1b6ff3492..19b866a63 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerExternal.cs +++ b/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerExternal.cs @@ -4,5 +4,5 @@ public class EventStoreTestServerExternal : IEventStoreTestServer { public Task StartAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; public void Stop() { } - public ValueTask DisposeAsync() => ValueTask.CompletedTask; -} \ No newline at end of file + public ValueTask DisposeAsync() => new ValueTask(Task.CompletedTask); +} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestCluster.cs b/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestCluster.cs index f97b852e2..ad8246843 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestCluster.cs +++ b/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestCluster.cs @@ -24,7 +24,6 @@ public static EventStoreFixtureOptions DefaultOptions() { ["EVENTSTORE_INT_TCP_PORT"] = "1112", ["EVENTSTORE_HTTP_PORT"] = "2113", ["EVENTSTORE_DISCOVER_VIA_DNS"] = "false", - ["EVENTSTORE_ENABLE_EXTERNAL_TCP"] = "false", ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000" }; @@ -50,4 +49,4 @@ protected override CompositeBuilder Configure() { protected override async Task OnServiceStarted() { await Service.WaitUntilNodesAreHealthy("esdb-node", TimeSpan.FromSeconds(60)); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs b/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs index 46ae316da..e767b3bae 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs +++ b/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http; using System.Net.Sockets; using Ductus.FluentDocker.Builders; using Ductus.FluentDocker.Common; @@ -31,7 +32,6 @@ public static EventStoreFixtureOptions DefaultOptions() { .With(x => x.ConnectivitySettings.DiscoveryInterval = FromSeconds(1)); var defaultEnvironment = new Dictionary(GlobalEnvironment.Variables) { - ["EVENTSTORE_ENABLE_EXTERNAL_TCP"] = "false", ["EVENTSTORE_MEM_DB"] = "true", ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024).ToString(), ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", @@ -82,7 +82,11 @@ protected override ContainerBuilder Configure() { protected override async Task OnServiceStarted() { using var http = new HttpClient( +#if NET new SocketsHttpHandler { SslOptions = { RemoteCertificateValidationCallback = delegate { return true; } } } +#else + new WinHttpHandler { ServerCertificateValidationCallback = delegate { return true; } } +#endif ) { BaseAddress = Options.ClientSettings.ConnectivitySettings.Address }; @@ -132,7 +136,11 @@ public async Task GetNextAvailablePort(TimeSpan delay = default) { await Task.Delay(delay); } finally { +#if NET if (socket.Connected) await socket.DisconnectAsync(true); +#else + if (socket.Connected) socket.Disconnect(true); +#endif } } } @@ -142,4 +150,4 @@ public async Task GetNextAvailablePort(TimeSpan delay = default) { } public int NextAvailablePort => GetNextAvailablePort(FromMilliseconds(100)).GetAwaiter().GetResult(); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs b/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs index e763edd35..c4773cede 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs +++ b/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + using Ductus.FluentDocker.Common; using Ductus.FluentDocker.Model.Containers; using Ductus.FluentDocker.Services; @@ -29,17 +31,32 @@ public static async ValueTask WaitUntilNodesAreHealthy(this IContainerService se public static async Task WaitUntilNodesAreHealthy(this ICompositeService service, IEnumerable services, CancellationToken cancellationToken) { var nodes = service.Containers.Where(x => services.Contains(x.Name)); - +#if NET await Parallel.ForEachAsync(nodes, cancellationToken, async (node, ct) => await node.WaitUntilNodesAreHealthy(ct)); +#else + Parallel.ForEach( + nodes, + node => { node.WaitUntilNodesAreHealthy(cancellationToken).GetAwaiter().GetResult(); } + ); +#endif } public static async Task WaitUntilNodesAreHealthy(this ICompositeService service, string serviceNamePrefix, CancellationToken cancellationToken) { var nodes = service.Containers.Where(x => x.Name.StartsWith(serviceNamePrefix)); + +#if NET await Parallel.ForEachAsync(nodes, cancellationToken, async (node, ct) => await node.WaitUntilNodesAreHealthy(ct)); +#else + Parallel.ForEach( + nodes, + node => { node.WaitUntilNodesAreHealthy(cancellationToken).GetAwaiter().GetResult(); } + ); +#endif } public static async Task WaitUntilNodesAreHealthy(this ICompositeService service, string serviceNamePrefix, TimeSpan timeout) { using var cts = new CancellationTokenSource(timeout); + await WaitUntilNodesAreHealthy(service, serviceNamePrefix, cts.Token); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/TestBypassService.cs b/test/EventStore.Client.Tests.Common/FluentDocker/TestBypassService.cs index 2fb3e805c..3505eb9af 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/TestBypassService.cs +++ b/test/EventStore.Client.Tests.Common/FluentDocker/TestBypassService.cs @@ -25,7 +25,7 @@ public override async Task Stop() { } } - public override ValueTask DisposeAsync() => ValueTask.CompletedTask; + public override ValueTask DisposeAsync() => new ValueTask(); } public sealed class BypassService : IService { @@ -58,4 +58,4 @@ public BypassBuilder() : this(null) { } public override BypassService Build() => new BypassService(); protected override IBuilder InternalCreate() => this; -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/TestService.cs b/test/EventStore.Client.Tests.Common/FluentDocker/TestService.cs index 026e0a12c..890f4e9b6 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/TestService.cs +++ b/test/EventStore.Client.Tests.Common/FluentDocker/TestService.cs @@ -1,6 +1,7 @@ using Ductus.FluentDocker.Builders; using Ductus.FluentDocker.Common; using Ductus.FluentDocker.Services; +using Google.Protobuf.WellKnownTypes; using Serilog; using static Serilog.Core.Constants; @@ -10,10 +11,10 @@ public interface ITestService : IAsyncDisposable { Task Start(); Task Stop(); Task Restart(TimeSpan delay); - - Task Restart() => Restart(TimeSpan.Zero); - - void ReportStatus(); + + Task Restart(); + + void ReportStatus(); } public abstract class TestService : ITestService where TService : IService where TBuilder : BaseBuilder { @@ -25,6 +26,11 @@ public abstract class TestService : ITestService where TServ INetworkService? Network { get; set; } = null!; + public Task Restart() + { + return Restart(TimeSpan.Zero); + } + public virtual async Task Start() { Logger.Information("Container service starting"); @@ -63,7 +69,7 @@ public virtual async Task Stop() { throw new FluentDockerException("Failed to stop container service", ex); } } - + public virtual async Task Restart(TimeSpan delay) { try { try { @@ -73,9 +79,9 @@ public virtual async Task Restart(TimeSpan delay) { catch (Exception ex) { throw new FluentDockerException("Failed to stop container service", ex); } - + await Task.Delay(delay); - + Logger.Information("Container service starting..."); try { @@ -132,11 +138,11 @@ public virtual ValueTask DisposeAsync() { throw new FluentDockerException("Failed to dispose of container service", ex); } - return ValueTask.CompletedTask; + return default; } protected abstract TBuilder Configure(); protected virtual Task OnServiceStarted() => Task.CompletedTask; protected virtual Task OnServiceStop() => Task.CompletedTask; -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs b/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs index f2e1e69eb..74f8838da 100644 --- a/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs +++ b/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs @@ -52,11 +52,11 @@ public static IDictionary GetEnvironmentVariables(IDictionary>()) { - if (key.StartsWith("EVENTSTORE") && !SharedEnv.Contains(key)) - throw new Exception($"Add {key} to shared.env and _sharedEnv to pass it to the cluster containers"); + foreach (var @override in overrides ?? Enumerable.Empty>()) { + if (@override.Key.StartsWith("EVENTSTORE") && !SharedEnv.Contains(@override.Key)) + throw new Exception($"Add {@override.Key} to shared.env and _sharedEnv to pass it to the cluster containers"); - env[key] = value; + env[@override.Key] = @override.Value; } return env; @@ -73,4 +73,4 @@ public static IDictionary GetEnvironmentVariables(IDictionary(handler); if (!tlsVerifyCert) { Assert.NotNull(socketsHandler.SslOptions.RemoteCertificateValidationCallback); @@ -128,6 +130,16 @@ public void tls_verify_cert(bool tlsVerifyCert) { else { Assert.Null(socketsHandler.SslOptions.RemoteCertificateValidationCallback); } +#else + var socketsHandler = Assert.IsType(handler); + if (!tlsVerifyCert) { + Assert.NotNull(socketsHandler.ServerCertificateValidationCallback); + Assert.True(socketsHandler.ServerCertificateValidationCallback!.Invoke(null!, default!, + default!, default)); + } else { + Assert.Null(socketsHandler.ServerCertificateValidationCallback); + } +#endif } #endif @@ -141,10 +153,15 @@ public void infinite_grpc_timeouts() { using var handler = result.CreateHttpMessageHandler?.Invoke(); +#if NET var socketsHandler = Assert.IsType(handler); - Assert.Equal(System.Threading.Timeout.InfiniteTimeSpan, socketsHandler.KeepAlivePingTimeout); Assert.Equal(System.Threading.Timeout.InfiniteTimeSpan, socketsHandler.KeepAlivePingDelay); +#else + var winHttpHandler = Assert.IsType(handler); + Assert.Equal(System.Threading.Timeout.InfiniteTimeSpan, winHttpHandler.TcpKeepAliveTime); + Assert.Equal(System.Threading.Timeout.InfiniteTimeSpan, winHttpHandler.TcpKeepAliveInterval); +#endif } [Fact] @@ -360,13 +377,19 @@ static string GetKeyValuePairs( settings.DefaultDeadline.Value.TotalMilliseconds.ToString() ); -#if !GRPC_CORE if (settings.CreateHttpMessageHandler != null) { using var handler = settings.CreateHttpMessageHandler.Invoke(); +#if NET if (handler is SocketsHttpHandler socketsHttpHandler && socketsHttpHandler.SslOptions.RemoteCertificateValidationCallback != null) pairs.Add("tlsVerifyCert", "false"); } +#else + if (handler is WinHttpHandler winHttpHandler && + winHttpHandler.ServerCertificateValidationCallback != null) { + pairs.Add("tlsVerifyCert", "false"); + } + } #endif return string.Join("&", pairs.Select(pair => $"{getKey?.Invoke(pair.Key) ?? pair.Key}={pair.Value}")); @@ -467,6 +490,7 @@ public bool Equals(EventStoreClientOperationOptions? x, EventStoreClientOperatio return x.GetType() == y.GetType(); } - public int GetHashCode(EventStoreClientOperationOptions obj) => System.HashCode.Combine(obj.ThrowOnAppendFailure); + public int GetHashCode(EventStoreClientOperationOptions obj) => + System.HashCode.Combine(obj.ThrowOnAppendFailure); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj b/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj index 0bd9cef57..494e5e243 100644 --- a/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj +++ b/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj @@ -1,27 +1,32 @@  - + - + - + - + - + + + + + - - - all runtime; build; native; contentfiles; analyzers + + + + diff --git a/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs b/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs index 9173f83b5..2b13cf13b 100644 --- a/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs +++ b/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs @@ -1,3 +1,4 @@ +#if NET using System.Net; using EventStore.Client.ServerFeatures; using Grpc.Core; @@ -97,4 +98,5 @@ class FakeServerFeatures : ServerFeatures.ServerFeatures.ServerFeaturesBase { public override Task GetSupportedMethods(Empty request, ServerCallContext context) => Task.FromResult(_supportedMethods); } -} \ No newline at end of file +} +#endif diff --git a/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj b/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj index d4b52e67f..abaf5e7bf 100644 --- a/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj +++ b/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj @@ -4,9 +4,9 @@ EventStore.Client.Tests - + - + \ No newline at end of file