Skip to content

Commit 73a031e

Browse files
authored
Net10 (#39)
* - add .net 10 support - update WeatherForecastWebApi sample project to .net 10 * - add .net 10 sdk to release github action * - update deps - update benchmark and weather forecast web apis to .net 10 - add documentation for server sent events with .net 10 -bump version * - add benchmark results for latest version
1 parent 69d26c9 commit 73a031e

File tree

21 files changed

+458
-73
lines changed

21 files changed

+458
-73
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727
dotnet-version: |
2828
8.0.x
2929
9.0.x
30+
10.0.x
3031
- name: Restore dependencies
3132
run: dotnet restore
3233
- name: Build

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@
1818
<PackageLicenseExpression>MIT</PackageLicenseExpression>
1919
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
2020

21-
<Version>1.3.5</Version>
21+
<Version>1.4.0</Version>
2222
</PropertyGroup>
2323
</Project>

README.md

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,7 @@ internal class HelloWorldRequestValidator : AbstractValidator<HelloWorldRequest>
117117
{
118118
public HelloWorldRequestValidator()
119119
{
120-
RuleFor(x => x.Name)
121-
.NotEmpty()
122-
.MinimumLength(3)
123-
.MaximumLength(50);
120+
RuleFor(x => x.Name).NotEmpty().MinimumLength(3).MaximumLength(50);
124121
}
125122
}
126123

@@ -151,19 +148,7 @@ internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary
151148

152149
internal class GetWeatherForecast : MinimalEndpoint<WeatherForecast[]>
153150
{
154-
private static readonly string[] _summaries =
155-
[
156-
"Freezing",
157-
"Bracing",
158-
"Chilly",
159-
"Cool",
160-
"Mild",
161-
"Warm",
162-
"Balmy",
163-
"Hot",
164-
"Sweltering",
165-
"Scorching"
166-
];
151+
private static readonly string[] _summaries = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];
167152

168153
protected override void Configure(
169154
EndpointConfigurationBuilder builder,

docs/IAsyncEnumerableResponse.md

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22

33
`MinimalEndpointWithStreamingResponse` is a specialized base class designed to simplify the implementation of minimal APIs that return streaming responses in .NET. It provides a structured way to define endpoints that stream data asynchronously using `IAsyncEnumerable<T>`.
44

5+
> **Note:** .Net 10 introduced `ServerSentEventsResult` IResult type to return IAsyncEnumerable responses which can be used by a `MinimalEndpoint`. See below for more information.
6+
57
``` csharp
6-
public record ListCustomersResponse(
7-
Guid Id,
8-
string FirstName,
9-
string? MiddleName,
10-
string LastName);
8+
public record ListCustomersResponse(Guid Id, string FirstName, string? MiddleName, string LastName);
119

1210
internal class ListCustomers(ServiceDbContext db)
1311
: MinimalEndpointWithStreamingResponse<ListCustomersResponse>
@@ -58,4 +56,91 @@ internal class ListStores(ServiceDbContext db)
5856
}
5957
}
6058
}
59+
```
60+
61+
## For .Net 10 or Later
62+
63+
By utilizing `ServerSentEventResult`, it's possible to return streaming responses with `MinimalEndpoint`.
64+
```csharp
65+
internal class GetStreamingWeatherForecastSse
66+
: MinimalEndpoint<IResult>
67+
{
68+
private static readonly string[] _summaries = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];
69+
70+
protected override void Configure(
71+
EndpointConfigurationBuilder builder,
72+
ConfigurationContext<EndpointConfigurationParameters> configurationContext)
73+
{
74+
builder.MapGet("/streamingweatherforecastsse")
75+
.WithName("GetStreamingWeatherForecastSse")
76+
.WithTags("WeatherForecastWebApi")
77+
.Produces<SseItem<WeatherForecast>>(contentType: "text/event-stream");
78+
}
79+
80+
protected override async Task<IResult> HandleAsync(CancellationToken ct)
81+
{
82+
await Task.CompletedTask;
83+
return Results.ServerSentEvents<WeatherForecast>(GetForecast(ct));
84+
85+
async IAsyncEnumerable<WeatherForecast> GetForecast([EnumeratorCancellation] CancellationToken ct)
86+
{
87+
var forecast = Enumerable.Range(1, 5).Select(index =>
88+
new WeatherForecast
89+
(
90+
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
91+
Random.Shared.Next(-20, 55),
92+
_summaries[Random.Shared.Next(_summaries.Length)]
93+
))
94+
.ToArray();
95+
96+
foreach (var item in forecast)
97+
{
98+
yield return item;
99+
await Task.Delay(500, ct);
100+
}
101+
}
102+
}
103+
}
104+
```
105+
106+
Instead of using `IResult` response type, it's also possible to use TypedResults for a more type safe approach.
107+
```csharp
108+
internal class GetStreamingWeatherForecastTypedSse
109+
: MinimalEndpoint<Results<ServerSentEventsResult<WeatherForecast>, ProblemHttpResult>>
110+
{
111+
private static readonly string[] _summaries = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];
112+
113+
protected override void Configure(
114+
EndpointConfigurationBuilder builder,
115+
ConfigurationContext<EndpointConfigurationParameters> configurationContext)
116+
{
117+
builder.MapGet("/streamingweatherforecasttypedsse")
118+
.WithName("GetStreamingWeatherForecastTypedSse")
119+
.WithTags("WeatherForecastWebApi");
120+
}
121+
122+
protected override async Task<Results<ServerSentEventsResult<WeatherForecast>, ProblemHttpResult>> HandleAsync(CancellationToken ct)
123+
{
124+
await Task.CompletedTask;
125+
return TypedResults.ServerSentEvents<WeatherForecast>(GetForecast(ct));
126+
127+
async IAsyncEnumerable<WeatherForecast> GetForecast([EnumeratorCancellation] CancellationToken ct)
128+
{
129+
var forecast = Enumerable.Range(1, 5).Select(index =>
130+
new WeatherForecast
131+
(
132+
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
133+
Random.Shared.Next(-20, 55),
134+
_summaries[Random.Shared.Next(_summaries.Length)]
135+
))
136+
.ToArray();
137+
138+
foreach (var item in forecast)
139+
{
140+
yield return item;
141+
await Task.Delay(500, ct);
142+
}
143+
}
144+
}
145+
}
61146
```
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
 k6  .\k6 run minimal_api_basic.js
2+
3+
/\ Grafana /‾‾/
4+
/\ / \ |\ __ / /
5+
/ \/ \ | |/ / / ‾‾\
6+
/ \ | ( | (‾) |
7+
/ __________ \ |_|\_\ \_____/
8+
9+
execution: local
10+
script: minimal_api_basic.js
11+
output: -
12+
13+
scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop):
14+
* default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)
15+
16+
17+
✓ status was 200
18+
19+
checks.........................: 100.00% 8933755 out of 8933755
20+
data_received..................: 1.5 GB 14 MB/s
21+
data_sent......................: 911 MB 8.3 MB/s
22+
http_req_blocked...............: avg=2.97µs min=0s med=0s max=30.58ms p(90)=0s p(95)=0s
23+
http_req_connecting............: avg=9ns min=0s med=0s max=2.99ms p(90)=0s p(95)=0s
24+
✓ http_req_duration..............: avg=836.33µs min=0s med=999.1µs max=49.71ms p(90)=1.67ms p(95)=2ms
25+
{ expected_response:true }...: avg=836.33µs min=0s med=999.1µs max=49.71ms p(90)=1.67ms p(95)=2ms
26+
http_req_failed................: 0.00% 0 out of 8933755
27+
http_req_receiving.............: avg=29.59µs min=0s med=0s max=41.83ms p(90)=0s p(95)=0s
28+
http_req_sending...............: avg=9.27µs min=0s med=0s max=33.06ms p(90)=0s p(95)=0s
29+
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
30+
http_req_waiting...............: avg=797.46µs min=0s med=998.8µs max=39.71ms p(90)=1.58ms p(95)=2ms
31+
http_reqs......................: 8933755 81215.461568/s
32+
iteration_duration.............: avg=934.51µs min=0s med=999.6µs max=72.4ms p(90)=1.99ms p(95)=2.02ms
33+
iterations.....................: 8933755 81215.461568/s
34+
vus............................: 1 min=1 max=100
35+
vus_max........................: 100 min=100 max=100
36+
37+
38+
running (1m50.0s), 000/100 VUs, 8933755 complete and 0 interrupted iterations
39+
default ✓ [======================================] 000/100 VUs 1m50s
40+
41+
42+
 k6  .\k6 run minimal_endpoint_basic.js
43+
44+
/\ Grafana /‾‾/
45+
/\ / \ |\ __ / /
46+
/ \/ \ | |/ / / ‾‾\
47+
/ \ | ( | (‾) |
48+
/ __________ \ |_|\_\ \_____/
49+
50+
execution: local
51+
script: minimal_endpoint_basic.js
52+
output: -
53+
54+
scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop):
55+
* default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)
56+
57+
58+
✓ status was 200
59+
60+
checks.........................: 100.00% 8791923 out of 8791923
61+
data_received..................: 1.5 GB 14 MB/s
62+
data_sent......................: 941 MB 8.6 MB/s
63+
http_req_blocked...............: avg=2.95µs min=0s med=0s max=36.05ms p(90)=0s p(95)=0s
64+
http_req_connecting............: avg=8ns min=0s med=0s max=2.54ms p(90)=0s p(95)=0s
65+
✓ http_req_duration..............: avg=861.35µs min=0s med=999.4µs max=42.99ms p(90)=1.69ms p(95)=2ms
66+
{ expected_response:true }...: avg=861.35µs min=0s med=999.4µs max=42.99ms p(90)=1.69ms p(95)=2ms
67+
http_req_failed................: 0.00% 0 out of 8791923
68+
http_req_receiving.............: avg=30.08µs min=0s med=0s max=40.5ms p(90)=0s p(95)=0s
69+
http_req_sending...............: avg=9.51µs min=0s med=0s max=41.34ms p(90)=0s p(95)=0s
70+
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
71+
http_req_waiting...............: avg=821.75µs min=0s med=999.1µs max=42.51ms p(90)=1.6ms p(95)=2ms
72+
http_reqs......................: 8791923 79926.279906/s
73+
iteration_duration.............: avg=951.89µs min=0s med=999.8µs max=58.78ms p(90)=1.99ms p(95)=2.01ms
74+
iterations.....................: 8791923 79926.279906/s
75+
vus............................: 1 min=1 max=100
76+
vus_max........................: 100 min=100 max=100
77+
78+
79+
running (1m50.0s), 000/100 VUs, 8791923 complete and 0 interrupted iterations
80+
default ✓ [======================================] 000/100 VUs 1m50s
81+
82+
83+
 k6  .\k6 run webresult_endpoint_basic.js
84+
85+
/\ Grafana /‾‾/
86+
/\ / \ |\ __ / /
87+
/ \/ \ | |/ / / ‾‾\
88+
/ \ | ( | (‾) |
89+
/ __________ \ |_|\_\ \_____/
90+
91+
execution: local
92+
script: webresult_endpoint_basic.js
93+
output: -
94+
95+
scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop):
96+
* default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)
97+
98+
99+
✓ status was 200
100+
101+
checks.........................: 100.00% 8763420 out of 8763420
102+
data_received..................: 1.5 GB 14 MB/s
103+
data_sent......................: 955 MB 8.7 MB/s
104+
http_req_blocked...............: avg=3µs min=0s med=0s max=31.14ms p(90)=0s p(95)=0s
105+
http_req_connecting............: avg=11ns min=0s med=0s max=16.58ms p(90)=0s p(95)=0s
106+
✓ http_req_duration..............: avg=867.84µs min=0s med=999.5µs max=110.19ms p(90)=1.73ms p(95)=2ms
107+
{ expected_response:true }...: avg=867.84µs min=0s med=999.5µs max=110.19ms p(90)=1.73ms p(95)=2ms
108+
http_req_failed................: 0.00% 0 out of 8763420
109+
http_req_receiving.............: avg=29.66µs min=0s med=0s max=42.15ms p(90)=0s p(95)=0s
110+
http_req_sending...............: avg=9.25µs min=0s med=0s max=52.18ms p(90)=0s p(95)=0s
111+
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
112+
http_req_waiting...............: avg=828.92µs min=0s med=999.2µs max=109.87ms p(90)=1.62ms p(95)=2ms
113+
http_reqs......................: 8763420 79666.408091/s
114+
iteration_duration.............: avg=958.27µs min=0s med=999.9µs max=110.7ms p(90)=1.99ms p(95)=2.01ms
115+
iterations.....................: 8763420 79666.408091/s
116+
vus............................: 1 min=1 max=100
117+
vus_max........................: 100 min=100 max=100
118+
119+
120+
running (1m50.0s), 000/100 VUs, 8763420 complete and 0 interrupted iterations
121+
default ✓ [======================================] 000/100 VUs 1m50s

0 commit comments

Comments
 (0)