Skip to content
This repository was archived by the owner on Sep 3, 2024. It is now read-only.

Commit afbd675

Browse files
etags for streams
1 parent 067bdd9 commit afbd675

9 files changed

Lines changed: 100 additions & 27 deletions

File tree

src/SqlStreamStore.HAL.Tests/StreamNavigationTests.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Net;
6+
using System.Net.Http.Headers;
67
using System.Threading.Tasks;
78
using Shouldly;
89
using SqlStreamStore.Streams;
@@ -37,6 +38,10 @@ public async Task read_head_link_no_messages(string stream, string baseAddress,
3738
using(var response = await _fixture.HttpClient.GetAsync($"{baseAddress}{firstResponse.Headers.Location}"))
3839
{
3940
response.StatusCode.ShouldBe(statusCode);
41+
var eTag = IsAllStream(stream)
42+
? ETag.FromPosition(Position.End)
43+
: ETag.FromStreamVersion(StreamVersion.End);
44+
response.Headers.ETag.ShouldBe(new EntityTagHeaderValue(eTag));
4045

4146
var resource = await response.AsHal();
4247

@@ -64,7 +69,7 @@ public static IEnumerable<object[]> GetPagingCases()
6469
[Theory, MemberData(nameof(GetPagingCases))]
6570
public async Task read_head_link_when_multiple_pages(string stream, string baseAddress)
6671
{
67-
await _fixture.WriteNMessages("a-stream", 30);
72+
var result = await _fixture.WriteNMessages("a-stream", 30);
6873

6974
using(var firstResponse = await _fixture.HttpClient.GetAsync($"{baseAddress}{stream}"))
7075
{
@@ -74,6 +79,10 @@ public async Task read_head_link_when_multiple_pages(string stream, string baseA
7479
await _fixture.HttpClient.GetAsync($"{baseAddress}{firstResponse.Headers.Location}"))
7580
{
7681
response.StatusCode.ShouldBe(HttpStatusCode.OK);
82+
var eTag = IsAllStream(stream)
83+
? ETag.FromPosition(result.CurrentPosition)
84+
: ETag.FromStreamVersion(result.CurrentVersion);
85+
response.Headers.ETag.ShouldBe(new EntityTagHeaderValue(eTag));
7786

7887
var resource = await response.AsHal();
7988

@@ -98,11 +107,15 @@ await _fixture.HttpClient.GetAsync($"{baseAddress}{firstResponse.Headers.Locatio
98107
[Theory, MemberData(nameof(GetPagingCases))]
99108
public async Task read_first_link(string stream, string baseAddress)
100109
{
101-
await _fixture.WriteNMessages("a-stream", 10);
110+
var result = await _fixture.WriteNMessages("a-stream", 10);
102111

103112
using(var response = await _fixture.HttpClient.GetAsync($"{baseAddress}{stream}?{FirstLinkQuery}"))
104113
{
105114
response.StatusCode.ShouldBe(HttpStatusCode.OK);
115+
var eTag = IsAllStream(stream)
116+
? ETag.FromPosition(result.CurrentPosition)
117+
: ETag.FromStreamVersion(result.CurrentVersion);
118+
response.Headers.ETag.ShouldBe(new EntityTagHeaderValue(eTag));
106119

107120
var resource = await response.AsHal();
108121

src/SqlStreamStore.HAL/ETag.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ internal struct ETag : IEquatable<ETag>
88

99
public static ETag FromPosition(long position) => new ETag($@"""{position}""");
1010
public static ETag FromStreamVersion(int streamVersion) => new ETag($@"""{streamVersion}""");
11+
public static readonly ETag None = default;
1112

1213
private ETag(string value)
1314
{
@@ -29,5 +30,6 @@ private ETag(string value)
2930
public static bool operator ==(ETag left, ETag right) => left.Equals(right);
3031
public static bool operator !=(ETag left, ETag right) => !left.Equals(right);
3132
public static implicit operator string(ETag etag) => etag._value;
33+
public static implicit operator string[](ETag etag) => new[] { etag._value };
3234
}
3335
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
namespace SqlStreamStore.HAL
2+
{
3+
using SqlStreamStore.Streams;
4+
5+
internal static class PageExtensions
6+
{
7+
public static bool TryGetETag(this ReadStreamPage page, out ETag eTag)
8+
{
9+
if(page.IsEnd)
10+
{
11+
eTag = ETag.FromStreamVersion(page.LastStreamVersion);
12+
return true;
13+
}
14+
15+
if(page.ReadDirection == ReadDirection.Backward && page.FromStreamVersion == StreamVersion.End)
16+
{
17+
eTag = ETag.FromStreamVersion(page.LastStreamVersion);
18+
return true;
19+
}
20+
21+
eTag = ETag.None;
22+
23+
return false;
24+
}
25+
26+
public static bool TryGetETag(this ReadAllPage page, long fromPosition, out ETag eTag)
27+
{
28+
if(page.Direction == ReadDirection.Backward && fromPosition == Position.End)
29+
{
30+
eTag = ETag.FromPosition(page.Messages.Length > 0 ? page.Messages[0].Position : Position.End);
31+
return true;
32+
}
33+
34+
if(page.IsEnd && page.Direction == ReadDirection.Forward)
35+
{
36+
eTag = ETag.FromPosition(page.Messages.Length > 0 ? page.Messages[page.Messages.Length - 1].Position : Position.End);
37+
return true;
38+
}
39+
40+
if(page.IsEnd && page.Direction == ReadDirection.Backward)
41+
{
42+
eTag = ETag.FromPosition(page.Messages.Length > 0 ? page.Messages[0].Position : Position.End);
43+
return true;
44+
}
45+
46+
eTag = ETag.None;
47+
48+
return false;
49+
}
50+
}
51+
}

src/SqlStreamStore.HAL/Resources/AllStreamResource.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ public async Task<Response> Get(
8989
response.Headers[Constants.Headers.HeadPosition] = new[] { $"{headPosition}" };
9090
}
9191

92+
if(page.TryGetETag(operation.FromPositionInclusive, out var eTag))
93+
{
94+
response.Headers[Constants.Headers.ETag] = eTag;
95+
}
96+
9297
return response;
9398
}
9499

src/SqlStreamStore.HAL/Resources/ReadStreamOperation.cs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,27 @@ public ReadStreamOperation(HttpRequest request)
2323
: Constants.ReadDirection.Backwards;
2424

2525
_fromVersionInclusive = request.Query.TryGetValueCaseInsensitive('p', out var position)
26-
? (int.TryParse(position, out _fromVersionInclusive)
27-
? (ReadDirection == Constants.ReadDirection.Forwards
28-
? (_fromVersionInclusive < StreamVersion.Start
26+
? int.TryParse(position, out _fromVersionInclusive)
27+
? ReadDirection == Constants.ReadDirection.Forwards
28+
? _fromVersionInclusive < StreamVersion.Start
2929
? StreamVersion.Start
30-
: _fromVersionInclusive)
31-
: (_fromVersionInclusive < StreamVersion.End
30+
: _fromVersionInclusive
31+
: _fromVersionInclusive < StreamVersion.End
3232
? StreamVersion.End
33-
: _fromVersionInclusive))
34-
: (ReadDirection == Constants.ReadDirection.Forwards
33+
: _fromVersionInclusive
34+
: ReadDirection == Constants.ReadDirection.Forwards
3535
? StreamVersion.Start
36-
: StreamVersion.End))
37-
: (ReadDirection == Constants.ReadDirection.Forwards
36+
: StreamVersion.End
37+
: ReadDirection == Constants.ReadDirection.Forwards
3838
? StreamVersion.Start
39-
: StreamVersion.End);
39+
: StreamVersion.End;
4040

4141
_maxCount = request.Query.TryGetValueCaseInsensitive('m', out var maxCount)
42-
? (int.TryParse(maxCount, out _maxCount)
43-
? (_maxCount <= 0
44-
? Constants.MaxCount
45-
: _maxCount)
46-
: Constants.MaxCount)
42+
? int.TryParse(maxCount, out _maxCount)
43+
? _maxCount <= 0
44+
? Constants.MaxCount
45+
: _maxCount
46+
: Constants.MaxCount
4747
: Constants.MaxCount;
4848

4949
Self = ReadDirection == Constants.ReadDirection.Forwards

src/SqlStreamStore.HAL/Resources/StreamMessageResource.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,7 @@ public async Task<Response> Get(
8383
{
8484
Headers =
8585
{
86-
[Constants.Headers.ETag] = new string[]
87-
{
88-
ETag.FromStreamVersion(message.StreamVersion)
89-
}
86+
[Constants.Headers.ETag] = ETag.FromStreamVersion(message.StreamVersion)
9087
}
9188
};
9289
}

src/SqlStreamStore.HAL/Resources/StreamMetadataResource.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,7 @@ public async Task<Response> Get(
5050
{
5151
Headers =
5252
{
53-
[Constants.Headers.ETag] = new string[]
54-
{
55-
ETag.FromStreamVersion(result.MetadataStreamVersion)
56-
}
53+
[Constants.Headers.ETag] = ETag.FromStreamVersion(result.MetadataStreamVersion)
5754
}
5855
};
5956

src/SqlStreamStore.HAL/Resources/StreamResource.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public async Task<Response> Get(ReadStreamOperation operation, CancellationToken
7272
? message.GetJsonData(cancellationToken)
7373
: SkippedPayload.Instance));
7474

75-
return new Response(
75+
var response = new Response(
7676
new HALResponse(new
7777
{
7878
page.LastStreamVersion,
@@ -112,6 +112,13 @@ public async Task<Response> Get(ReadStreamOperation operation, CancellationToken
112112
Links.Message.Self(message),
113113
Links.Message.Feed(message)))),
114114
page.Status == PageReadStatus.StreamNotFound ? 404 : 200);
115+
116+
if (page.TryGetETag(out var eTag))
117+
{
118+
response.Headers[Constants.Headers.ETag] = eTag;
119+
}
120+
121+
return response;
115122
}
116123

117124
public async Task<Response> Delete(DeleteStreamOperation operation, CancellationToken cancellationToken)
@@ -124,7 +131,7 @@ public async Task<Response> Delete(DeleteStreamOperation operation, Cancellation
124131
private static class Links
125132
{
126133
public static Link Find() => SqlStreamStore.HAL.Links.Find("../streams/{streamId}");
127-
134+
128135
public static Link First(ReadStreamPage page, ReadStreamOperation operation)
129136
=> new Link(
130137
Constants.Relations.First,

src/SqlStreamStore.HAL/SqlStreamStore.HAL.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<AssemblyTitle>Stream Store - HAL Server</AssemblyTitle>
77
<DebugSymbols>true</DebugSymbols>
88
<NoWarn>1701;1702;1705;1591</NoWarn>
9+
<LangVersion>latest</LangVersion>
910
</PropertyGroup>
1011
<ItemGroup>
1112
<PackageReference Include="Halcyon" Version="2.5.1" />

0 commit comments

Comments
 (0)