Skip to content

Commit be8c8f5

Browse files
authored
Merge pull request #55 from tpeczek/46-controlling-format-of-keep-alive
Adding options to configure the format of keepalive. Resolves #46
2 parents 5de3000 + e27e800 commit be8c8f5

19 files changed

+342
-20
lines changed

DocFx.AspNetCore.ServerSentEvents/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ You can install [Lib.AspNetCore.ServerSentEvents](https://www.nuget.org/packages
1212
PM> Install-Package Lib.AspNetCore.ServerSentEvents
1313
```
1414

15-
The configuration and basic usage patterns are described [here](articles/getting-started.md).
15+
The configuration and basic usage patterns are described [here](articles/getting-started.html).
1616

1717
## Demos
1818

Lib.AspNetCore.ServerSentEvents/Lib.AspNetCore.ServerSentEvents.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<PackageTags>aspnetcore;sse;server-sent;events;eventsource</PackageTags>
1313
<PackageProjectUrl>https://github.com/tpeczek/Lib.AspNetCore.ServerSentEvents</PackageProjectUrl>
1414
<PackageLicenseExpression>MIT</PackageLicenseExpression>
15+
<PackageReadmeFile>README.md</PackageReadmeFile>
1516
<RepositoryType>git</RepositoryType>
1617
<RepositoryUrl>git://github.com/tpeczek/Lib.AspNetCore.ServerSentEvents</RepositoryUrl>
1718
<GenerateAssemblyTitleAttribute>true</GenerateAssemblyTitleAttribute>

Lib.AspNetCore.ServerSentEvents/ServerSentEventsEndpointRouteBuilderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#if !NETCOREAPP2_1 && !NET461
1+
#if !NET461
22
using System;
33
using Microsoft.AspNetCore.Http;
44
using Microsoft.AspNetCore.Builder;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace Lib.AspNetCore.ServerSentEvents
2+
{
3+
/// <summary>
4+
/// The kind of content for keepalive.
5+
/// </summary>
6+
public enum ServerSentEventsKeepaliveKind
7+
{
8+
/// <summary>
9+
/// The keepalive will be send as a comment.
10+
/// </summary>
11+
Comment,
12+
/// <summary>
13+
/// The keepalive will be send as an event.
14+
/// </summary>
15+
Event
16+
}
17+
}

Lib.AspNetCore.ServerSentEvents/ServerSentEventsKeepaliveService.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Threading;
33
using System.Threading.Tasks;
4+
using System.Collections.Generic;
45
using Microsoft.Extensions.Hosting;
56
using Microsoft.Extensions.Options;
67
using Lib.AspNetCore.ServerSentEvents.Internals;
@@ -12,7 +13,7 @@ internal class ServerSentEventsKeepaliveService<TServerSentEventsService> : IHos
1213
{
1314
#region Fields
1415
private readonly bool _isBehindAncm = IsBehindAncm();
15-
private readonly static ServerSentEventBytes _keepaliveServerSentEventBytes = ServerSentEventsHelper.GetCommentBytes("KEEPALIVE");
16+
private readonly ServerSentEventBytes _keepaliveServerSentEventBytes;
1617

1718
private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();
1819

@@ -26,7 +27,11 @@ internal class ServerSentEventsKeepaliveService<TServerSentEventsService> : IHos
2627
public ServerSentEventsKeepaliveService(TServerSentEventsService serverSentEventsService, IOptions<ServerSentEventsServiceOptions<TServerSentEventsService>> options)
2728
{
2829
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
30+
2931
_serverSentEventsService = serverSentEventsService;
32+
_keepaliveServerSentEventBytes = (_options.KeepaliveKind == ServerSentEventsKeepaliveKind.Comment)
33+
? ServerSentEventsHelper.GetCommentBytes(_options.KeepaliveContent)
34+
: ServerSentEventsHelper.GetEventBytes(new ServerSentEvent { Type = _options.KeepaliveContent, Data = new List<string> { String.Empty } });
3035
}
3136
#endregion
3237

Lib.AspNetCore.ServerSentEvents/ServerSentEventsMiddleware.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ private async Task ForbidAsync(HttpContext context)
217217

218218
private void DisableResponseBuffering(HttpContext context)
219219
{
220-
#if !NETCOREAPP2_1 && !NET461
220+
#if !NET461
221221
IHttpResponseBodyFeature responseBodyFeature = context.Features.Get<IHttpResponseBodyFeature>();
222222
if (responseBodyFeature != null)
223223
{

Lib.AspNetCore.ServerSentEvents/ServerSentEventsServiceOptions.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ namespace Lib.AspNetCore.ServerSentEvents
88
/// </summary>
99
public class ServerSentEventsServiceOptions<TServerSentEventsService> where TServerSentEventsService : ServerSentEventsService
1010
{
11-
internal const int DEFAULT_KEEPALIVE_INTERVAL = 30;
11+
private const int DEFAULT_KEEPALIVE_INTERVAL = 30;
12+
private const string DEFAULT_KEEPALIVE_CONTENT = "KEEPALIVE";
1213

1314
private int _keepaliveInterval = DEFAULT_KEEPALIVE_INTERVAL;
15+
private string _keepaliveContent = DEFAULT_KEEPALIVE_CONTENT;
1416

1517
/// <summary>
1618
/// Gets or sets the keepalive sending mode.
@@ -35,6 +37,29 @@ public int KeepaliveInterval
3537
}
3638
}
3739

40+
/// <summary>
41+
/// Gets or sets the kind of content for keepalive.
42+
/// </summary>
43+
public ServerSentEventsKeepaliveKind KeepaliveKind { get; set; } = ServerSentEventsKeepaliveKind.Comment;
44+
45+
/// <summary>
46+
/// Gets or sets the content for keepalive. If the <see cref="KeepaliveKind"/> is <see cref="ServerSentEventsKeepaliveKind.Comment"/> it will be the content of the comment. If the <see cref="KeepaliveKind"/> is <see cref="ServerSentEventsKeepaliveKind.Event"/> it will be the type of the event.
47+
/// </summary>
48+
public string KeepaliveContent
49+
{
50+
get { return _keepaliveContent; }
51+
52+
set
53+
{
54+
if (String.IsNullOrWhiteSpace(value))
55+
{
56+
throw new ArgumentNullException(nameof(value));
57+
}
58+
59+
_keepaliveContent = value;
60+
}
61+
}
62+
3863
/// <summary>
3964
/// Gets or sets the interval after which clients will attempt to reestablish failed connections.
4065
/// </summary>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System;
2+
using Microsoft.AspNetCore.Builder;
3+
using Microsoft.Extensions.Configuration;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Lib.AspNetCore.ServerSentEvents;
6+
7+
namespace Test.AspNetCore.ServerSentEvents.Functional.Infrastructure
8+
{
9+
internal abstract class FakeServerSentEventsServerStartup
10+
{
11+
public const string SERVER_SENT_EVENTS_ENDPOINT = "/sse";
12+
13+
protected abstract Action<ServerSentEventsServiceOptions<ServerSentEventsService>> ConfigureServerSentEventsOption { get; }
14+
15+
public IConfiguration Configuration { get; }
16+
17+
public FakeServerSentEventsServerStartup(IConfiguration configuration)
18+
{
19+
Configuration = configuration;
20+
}
21+
22+
public void ConfigureServices(IServiceCollection services)
23+
{
24+
services.AddServerSentEvents(ConfigureServerSentEventsOption);
25+
}
26+
27+
#if !NET461
28+
public void Configure(IApplicationBuilder app)
29+
{
30+
app.UseRouting()
31+
.UseEndpoints(endpoints =>
32+
{
33+
endpoints.MapServerSentEvents(SERVER_SENT_EVENTS_ENDPOINT);
34+
});
35+
}
36+
#else
37+
public void Configure(IApplicationBuilder app)
38+
{
39+
app.MapServerSentEvents(SERVER_SENT_EVENTS_ENDPOINT);
40+
}
41+
#endif
42+
}
43+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.IO;
2+
using Microsoft.AspNetCore;
3+
using Microsoft.AspNetCore.Hosting;
4+
using Microsoft.AspNetCore.Mvc.Testing;
5+
6+
namespace Test.AspNetCore.ServerSentEvents.Functional.Infrastructure
7+
{
8+
internal class FakeServerSentEventsServerrApplicationFactory<TFakeServerSentEventsServerStartup> : WebApplicationFactory<TFakeServerSentEventsServerStartup> where TFakeServerSentEventsServerStartup : FakeServerSentEventsServerStartup
9+
{
10+
#if !NET461
11+
protected override IWebHostBuilder CreateWebHostBuilder()
12+
{
13+
return WebHost.CreateDefaultBuilder()
14+
.UseStartup<TFakeServerSentEventsServerStartup>();
15+
}
16+
#else
17+
protected override IWebHostBuilder CreateWebHostBuilder()
18+
{
19+
return new WebHostBuilder()
20+
.UseStartup<TFakeServerSentEventsServerStartup>();
21+
}
22+
#endif
23+
24+
protected override void ConfigureWebHost(IWebHostBuilder builder)
25+
{
26+
builder.UseContentRoot(Path.GetTempPath());
27+
}
28+
}
29+
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
using System;
2+
using System.IO;
3+
using System.Text;
4+
using System.Buffers;
5+
using System.Net.Http;
6+
using System.Threading;
7+
using System.Diagnostics;
8+
using System.Threading.Tasks;
9+
using Microsoft.Extensions.Configuration;
10+
using Xunit;
11+
using Lib.AspNetCore.ServerSentEvents;
12+
using Test.AspNetCore.ServerSentEvents.Functional.Infrastructure;
13+
14+
namespace Test.AspNetCore.ServerSentEvents.Functional
15+
{
16+
public class KeepAliveTests
17+
{
18+
#region Fields
19+
private const int KEEPALIVE_INTERVAL = 1;
20+
private readonly static TimeSpan KEEPALIVE_TIMESPAN = TimeSpan.FromSeconds(KEEPALIVE_INTERVAL + 1);
21+
22+
private const string DEFAULT_KEEPALIVE = ": KEEPALIVE\r\n\r\n";
23+
private const string CUSTOM_KEEPALIVE_CONTENT = "PING";
24+
private const string CUSTOM_KEEPALIVE_COMMENT = ": PING\r\n\r\n";
25+
private const string CUSTOM_KEEPALIVE_EVENT = "event: PING\r\ndata: \r\n\r\n";
26+
#endregion
27+
28+
#region SUT
29+
private class KeepaliveNeverServerSentEventsServerStartup : FakeServerSentEventsServerStartup
30+
{
31+
public KeepaliveNeverServerSentEventsServerStartup(IConfiguration configuration) : base(configuration)
32+
{ }
33+
34+
protected override Action<ServerSentEventsServiceOptions<ServerSentEventsService>> ConfigureServerSentEventsOption
35+
{
36+
get
37+
{
38+
return options =>
39+
{
40+
options.KeepaliveMode = ServerSentEventsKeepaliveMode.Never;
41+
options.KeepaliveInterval = KEEPALIVE_INTERVAL;
42+
};
43+
}
44+
}
45+
}
46+
47+
private class KeepaliveDefultAlwaysServerSentEventsServerStartup : FakeServerSentEventsServerStartup
48+
{
49+
public KeepaliveDefultAlwaysServerSentEventsServerStartup(IConfiguration configuration) : base(configuration)
50+
{ }
51+
52+
protected override Action<ServerSentEventsServiceOptions<ServerSentEventsService>> ConfigureServerSentEventsOption
53+
{
54+
get
55+
{
56+
return options =>
57+
{
58+
options.KeepaliveMode = ServerSentEventsKeepaliveMode.Always;
59+
options.KeepaliveInterval = KEEPALIVE_INTERVAL;
60+
};
61+
}
62+
}
63+
}
64+
65+
private class KeepaliveCustomCommentAlwaysServerSentEventsServerStartup : FakeServerSentEventsServerStartup
66+
{
67+
public KeepaliveCustomCommentAlwaysServerSentEventsServerStartup(IConfiguration configuration) : base(configuration)
68+
{ }
69+
70+
protected override Action<ServerSentEventsServiceOptions<ServerSentEventsService>> ConfigureServerSentEventsOption
71+
{
72+
get
73+
{
74+
return options =>
75+
{
76+
options.KeepaliveMode = ServerSentEventsKeepaliveMode.Always;
77+
options.KeepaliveInterval = KEEPALIVE_INTERVAL;
78+
options.KeepaliveKind = ServerSentEventsKeepaliveKind.Comment;
79+
options.KeepaliveContent = CUSTOM_KEEPALIVE_CONTENT;
80+
};
81+
}
82+
}
83+
}
84+
85+
private class KeepaliveCustomEventAlwaysServerSentEventsServerStartup : FakeServerSentEventsServerStartup
86+
{
87+
public KeepaliveCustomEventAlwaysServerSentEventsServerStartup(IConfiguration configuration) : base(configuration)
88+
{ }
89+
90+
protected override Action<ServerSentEventsServiceOptions<ServerSentEventsService>> ConfigureServerSentEventsOption
91+
{
92+
get
93+
{
94+
return options =>
95+
{
96+
options.KeepaliveMode = ServerSentEventsKeepaliveMode.Always;
97+
options.KeepaliveInterval = KEEPALIVE_INTERVAL;
98+
options.KeepaliveKind = ServerSentEventsKeepaliveKind.Event;
99+
options.KeepaliveContent = CUSTOM_KEEPALIVE_CONTENT;
100+
};
101+
}
102+
}
103+
}
104+
#endregion
105+
106+
#region Tests
107+
[Fact]
108+
public async Task ServerSentEventsServer_KeepaliveModeNever_DoesNotSendKeepalive()
109+
{
110+
using FakeServerSentEventsServerrApplicationFactory<KeepaliveNeverServerSentEventsServerStartup> serverSentEventsServerApplicationFactory = new();
111+
HttpClient serverSentEventsClient = serverSentEventsServerApplicationFactory.CreateClient();
112+
113+
string serverSentEvents = await GetServerSentEvents(serverSentEventsClient).ConfigureAwait(false);
114+
115+
Assert.Equal(String.Empty, serverSentEvents);
116+
}
117+
118+
[Fact]
119+
public async Task ServerSentEventsServer_KeepaliveModeAlways_SendsDefaultKeepalive()
120+
{
121+
using FakeServerSentEventsServerrApplicationFactory<KeepaliveDefultAlwaysServerSentEventsServerStartup> serverSentEventsServerApplicationFactory = new ();
122+
HttpClient serverSentEventsClient = serverSentEventsServerApplicationFactory.CreateClient();
123+
124+
string serverSentEvents = await GetServerSentEvents(serverSentEventsClient).ConfigureAwait(false);
125+
126+
Assert.Matches($"^({DEFAULT_KEEPALIVE})+$", serverSentEvents);
127+
}
128+
129+
[Fact]
130+
public async Task ServerSentEventsServer_KeepaliveModeAlwaysKeepaliveKindCommentKeepaliveContentCustom_SendsCustomCommentKeepalive()
131+
{
132+
using FakeServerSentEventsServerrApplicationFactory<KeepaliveCustomCommentAlwaysServerSentEventsServerStartup> serverSentEventsServerApplicationFactory = new();
133+
HttpClient serverSentEventsClient = serverSentEventsServerApplicationFactory.CreateClient();
134+
135+
string serverSentEvents = await GetServerSentEvents(serverSentEventsClient).ConfigureAwait(false);
136+
137+
Assert.Matches($"^({CUSTOM_KEEPALIVE_COMMENT})+$", serverSentEvents);
138+
}
139+
140+
[Fact]
141+
public async Task ServerSentEventsServer_KeepaliveModeAlwaysKeepaliveKindEventKeepaliveContentCustom_SendsCustomEventKeepalive()
142+
{
143+
using FakeServerSentEventsServerrApplicationFactory<KeepaliveCustomEventAlwaysServerSentEventsServerStartup> serverSentEventsServerApplicationFactory = new();
144+
HttpClient serverSentEventsClient = serverSentEventsServerApplicationFactory.CreateClient();
145+
146+
string serverSentEvents = await GetServerSentEvents(serverSentEventsClient).ConfigureAwait(false);
147+
148+
Assert.Matches($"^({CUSTOM_KEEPALIVE_EVENT})+$", serverSentEvents);
149+
}
150+
151+
private static async Task<string> GetServerSentEvents(HttpClient serverSentEventsClient)
152+
{
153+
Stopwatch keepaliveStopwatch = new Stopwatch();
154+
string serverSentEventsResponseContent = String.Empty;
155+
156+
serverSentEventsClient.DefaultRequestHeaders.Add("Accept", "text/event-stream");
157+
using (HttpResponseMessage serverSentEventsResponse = await serverSentEventsClient.GetAsync(FakeServerSentEventsServerStartup.SERVER_SENT_EVENTS_ENDPOINT, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
158+
{
159+
serverSentEventsResponse.EnsureSuccessStatusCode();
160+
161+
keepaliveStopwatch.Start();
162+
163+
using (Stream responseStream = await serverSentEventsResponse.Content.ReadAsStreamAsync().ConfigureAwait(false))
164+
{
165+
do
166+
{
167+
byte[] buffer = ArrayPool<byte>.Shared.Rent(128);
168+
169+
try
170+
{
171+
using CancellationTokenSource keepaliveCancellationTokenSource = new CancellationTokenSource(KEEPALIVE_TIMESPAN);
172+
173+
int bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length, keepaliveCancellationTokenSource.Token).ConfigureAwait(false);
174+
serverSentEventsResponseContent += Encoding.UTF8.GetString(buffer, 0, bytesRead);
175+
}
176+
catch (Exception ex) when (ex is OperationCanceledException || ex.InnerException is OperationCanceledException)
177+
{ }
178+
179+
ArrayPool<byte>.Shared.Return(buffer);
180+
} while (keepaliveStopwatch.Elapsed < KEEPALIVE_TIMESPAN);
181+
}
182+
}
183+
184+
keepaliveStopwatch.Stop();
185+
186+
return serverSentEventsResponseContent;
187+
}
188+
#endregion
189+
}
190+
}

0 commit comments

Comments
 (0)