Skip to content

Commit 87c2931

Browse files
committed
feat: add ToServerSentEventsHttpResult with IAsyncEnumerableT extension method
1 parent 1b55f15 commit 87c2931

9 files changed

Lines changed: 421 additions & 32 deletions

File tree

CSharpFunctionalExtensions.HttpResults.Generators/Builders/ResultExtensionsClassBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ List<ClassDeclarationSyntax> mapperClasses
3030
new ToStatusCodeHttpResultTE(),
3131
new ToOkHttpResultTE(),
3232
new ToContentHttpResultStringE(),
33+
new ToServerSentEventsHttpResultIAsyncEnumerableTE(),
3334
];
3435
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace CSharpFunctionalExtensions.HttpResults.Generators.ResultExtensions;
2+
3+
internal class ToServerSentEventsHttpResultIAsyncEnumerableTE : IGenerateMethods
4+
{
5+
public string Generate(string mapperClassName, string resultErrorType, string httpResultType)
6+
{
7+
return $$"""
8+
#if NET10_0_OR_GREATER
9+
/// <summary>
10+
/// Returns a <see cref="ServerSentEventsResult{T}" /> based of a <see cref="IAsyncEnumerable{T}" /> in case of success. Returns custom mapping in case of failure.
11+
/// </summary>
12+
public static Results<ServerSentEventsResult<T>, {{httpResultType}}> ToServerSentEventsHttpResult<T>(this Result<IAsyncEnumerable<T>,{{resultErrorType}}> result, string? eventType = null)
13+
{
14+
if (result.IsSuccess) return TypedResults.ServerSentEvents(result.Value, eventType);
15+
16+
return ErrorMapperInstances.{{mapperClassName}}.Map(result.Error);
17+
}
18+
19+
/// <summary>
20+
/// Returns a <see cref="ServerSentEventsResult{T}" /> based of a <see cref="IAsyncEnumerable{T}" /> in case of success. Returns custom mapping in case of failure.
21+
/// </summary>
22+
public static async Task<Results<ServerSentEventsResult<T>, {{httpResultType}}>> ToServerSentEventsHttpResult<T>(this Task<Result<IAsyncEnumerable<T>,{{resultErrorType}}>> result, string? eventType = null)
23+
{
24+
return (await result).ToServerSentEventsHttpResult(eventType);
25+
}
26+
#endif
27+
""";
28+
}
29+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#if NET10_0_OR_GREATER
2+
3+
using System.Net.Mime;
4+
using CSharpFunctionalExtensions.HttpResults.ResultExtensions;
5+
using CSharpFunctionalExtensions.HttpResults.Tests.Utils;
6+
using FluentAssertions;
7+
using Microsoft.AspNetCore.Http.HttpResults;
8+
9+
namespace CSharpFunctionalExtensions.HttpResults.Tests.ResultExtensions;
10+
11+
public class ToServerSentEventsHttpResultIAsyncEnumerableT
12+
{
13+
[Fact]
14+
public async Task ResultIAsyncEnumerableT_Success_can_be_mapped_to_ServerSentEventsHttpResult()
15+
{
16+
var testValues = new[] { 42, 420 };
17+
var eventType = "TestEvent";
18+
19+
var asyncEnumerable = testValues.AsAsyncEnumerable();
20+
21+
var result =
22+
Result.Success(asyncEnumerable).ToServerSentEventsHttpResult(eventType).Result as ServerSentEventsResult<int>;
23+
24+
var (response, values) = await result!.ExecuteAndGetResponseAndValues();
25+
26+
result!.StatusCode.Should().Be(200);
27+
response.ContentType.Should().Be(MediaTypeNames.Text.EventStream);
28+
values.Select(v => v.Data).Should().Contain(testValues.Select(v => v.ToString()));
29+
values.Select(v => v.Event).Should().AllBe(eventType);
30+
}
31+
32+
[Fact]
33+
public async Task ResultIAsyncEnumerableT_Success_can_be_mapped_to_ServerSentEventsHttpResult_Async()
34+
{
35+
var testValues = new[] { 42, 420 };
36+
var eventType = "TestEvent";
37+
38+
var asyncEnumerable = testValues.AsAsyncEnumerable();
39+
40+
var result =
41+
(await Task.FromResult(Result.Success(asyncEnumerable)).ToServerSentEventsHttpResult(eventType)).Result
42+
as ServerSentEventsResult<int>;
43+
44+
var (response, values) = await result!.ExecuteAndGetResponseAndValues();
45+
46+
result!.StatusCode.Should().Be(200);
47+
response.ContentType.Should().Be(MediaTypeNames.Text.EventStream);
48+
values.Select(v => v.Data).Should().Contain(testValues.Select(v => v.ToString()));
49+
values.Select(v => v.Event).Should().AllBe(eventType);
50+
}
51+
52+
[Fact]
53+
public void ResultIAsyncEnumerableT_Failure_can_be_mapped_to_ServerSentEventsHttpResult()
54+
{
55+
var error = "Error";
56+
57+
var result =
58+
Result.Failure<IAsyncEnumerable<int>>(error).ToServerSentEventsHttpResult().Result as ProblemHttpResult;
59+
60+
result!.StatusCode.Should().Be(400);
61+
result!.ProblemDetails.Status.Should().Be(400);
62+
result!.ProblemDetails.Detail.Should().Be(error);
63+
}
64+
65+
[Fact]
66+
public async Task ResultIAsyncEnumerableT_Failure_can_be_mapped_to_ServerSentEventsHttpResult_Async()
67+
{
68+
var error = "Error";
69+
70+
var result =
71+
(await Task.FromResult(Result.Failure<IAsyncEnumerable<int>>(error)).ToServerSentEventsHttpResult()).Result
72+
as ProblemHttpResult;
73+
74+
result!.StatusCode.Should().Be(400);
75+
result!.ProblemDetails.Status.Should().Be(400);
76+
result!.ProblemDetails.Detail.Should().Be(error);
77+
}
78+
79+
[Fact]
80+
public void ResultIAsyncEnumerableT_Failure_StatusCode_can_be_changed()
81+
{
82+
var statusCode = 418;
83+
var error = "Error";
84+
85+
var result =
86+
Result.Failure<IAsyncEnumerable<int>>(error).ToServerSentEventsHttpResult(failureStatusCode: statusCode).Result
87+
as ProblemHttpResult;
88+
89+
result!.StatusCode.Should().Be(statusCode);
90+
result!.ProblemDetails.Status.Should().Be(statusCode);
91+
result!.ProblemDetails.Detail.Should().Be(error);
92+
}
93+
94+
[Fact]
95+
public async Task ResultIAsyncEnumerableT_Failure_StatusCode_can_be_changed_Async()
96+
{
97+
var statusCode = 418;
98+
var error = "Error";
99+
100+
var result =
101+
(
102+
await Task.FromResult(Result.Failure<IAsyncEnumerable<int>>(error))
103+
.ToServerSentEventsHttpResult(failureStatusCode: statusCode)
104+
).Result as ProblemHttpResult;
105+
106+
result!.StatusCode.Should().Be(statusCode);
107+
result!.ProblemDetails.Status.Should().Be(statusCode);
108+
result!.ProblemDetails.Detail.Should().Be(error);
109+
}
110+
111+
[Fact]
112+
public void ResultIAsyncEnumerableT_Failure_ProblemDetails_can_be_customized()
113+
{
114+
var error = "Error";
115+
var customTitle = "Custom Title";
116+
117+
var result =
118+
Result
119+
.Failure<IAsyncEnumerable<int>>(error)
120+
.ToServerSentEventsHttpResult(customizeProblemDetails: problemDetails => problemDetails.Title = customTitle)
121+
.Result as ProblemHttpResult;
122+
123+
result!.ProblemDetails.Title.Should().Be(customTitle);
124+
}
125+
126+
[Fact]
127+
public async Task ResultIAsyncEnumerableT_Failure_ProblemDetails_can_be_customized_Async()
128+
{
129+
var error = "Error";
130+
var customTitle = "Custom Title";
131+
132+
var result =
133+
(
134+
await Task.FromResult(Result.Failure<IAsyncEnumerable<int>>(error))
135+
.ToServerSentEventsHttpResult(customizeProblemDetails: problemDetails => problemDetails.Title = customTitle)
136+
).Result as ProblemHttpResult;
137+
138+
result!.ProblemDetails.Title.Should().Be(customTitle);
139+
}
140+
}
141+
142+
#endif
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#if NET10_0_OR_GREATER
2+
3+
using System.Net.Mime;
4+
using CSharpFunctionalExtensions.HttpResults.ResultExtensions;
5+
using CSharpFunctionalExtensions.HttpResults.Tests.Shared;
6+
using CSharpFunctionalExtensions.HttpResults.Tests.Utils;
7+
using FluentAssertions;
8+
using Microsoft.AspNetCore.Http.HttpResults;
9+
10+
namespace CSharpFunctionalExtensions.HttpResults.Tests.ResultExtensions;
11+
12+
public class ToServerSentEventsHttpResultIAsyncEnumerableTE
13+
{
14+
[Fact]
15+
public async Task ResultIAsyncEnumerableTE_Success_can_be_mapped_to_ServerSentEventsHttpResult()
16+
{
17+
var testValues = new[] { 42, 420 };
18+
var eventType = "TestEvent";
19+
20+
var asyncEnumerable = testValues.AsAsyncEnumerable();
21+
22+
var result =
23+
Result
24+
.Success<IAsyncEnumerable<int>, DocumentMissingError>(asyncEnumerable)
25+
.ToServerSentEventsHttpResult(eventType)
26+
.Result as ServerSentEventsResult<int>;
27+
28+
var (response, values) = await result!.ExecuteAndGetResponseAndValues();
29+
30+
result!.StatusCode.Should().Be(200);
31+
response.ContentType.Should().Be(MediaTypeNames.Text.EventStream);
32+
values.Select(v => v.Data).Should().Contain(testValues.Select(v => v.ToString()));
33+
values.Select(v => v.Event).Should().AllBe(eventType);
34+
}
35+
36+
[Fact]
37+
public async Task ResultIAsyncEnumerableTE_Success_can_be_mapped_to_ServerSentEventsHttpResult_Async()
38+
{
39+
var testValues = new[] { 42, 420 };
40+
var eventType = "TestEvent";
41+
42+
var asyncEnumerable = testValues.AsAsyncEnumerable();
43+
44+
var result =
45+
(
46+
await Task.FromResult(Result.Success<IAsyncEnumerable<int>, DocumentMissingError>(asyncEnumerable))
47+
.ToServerSentEventsHttpResult(eventType)
48+
).Result as ServerSentEventsResult<int>;
49+
50+
var (response, values) = await result!.ExecuteAndGetResponseAndValues();
51+
52+
result!.StatusCode.Should().Be(200);
53+
response.ContentType.Should().Be(MediaTypeNames.Text.EventStream);
54+
values.Select(v => v.Data).Should().Contain(testValues.Select(v => v.ToString()));
55+
values.Select(v => v.Event).Should().AllBe(eventType);
56+
}
57+
58+
[Fact]
59+
public void ResultIAsyncEnumerableTE_Failure_can_be_mapped_to_ServerSentEventsHttpResult()
60+
{
61+
var error = new DocumentMissingError { DocumentId = Guid.NewGuid().ToString() };
62+
63+
var result =
64+
Result.Failure<IAsyncEnumerable<int>, DocumentMissingError>(error).ToServerSentEventsHttpResult().Result
65+
as NotFound<string>;
66+
67+
result!.StatusCode.Should().Be(404);
68+
result!.Value.Should().Be(error.DocumentId);
69+
}
70+
71+
[Fact]
72+
public async Task ResultIAsyncEnumerableTE_Failure_can_be_mapped_to_ServerSentEventsHttpResult_Async()
73+
{
74+
var error = new DocumentMissingError { DocumentId = Guid.NewGuid().ToString() };
75+
76+
var result =
77+
(
78+
await Task.FromResult(Result.Failure<IAsyncEnumerable<int>, DocumentMissingError>(error))
79+
.ToServerSentEventsHttpResult()
80+
).Result as NotFound<string>;
81+
82+
result!.StatusCode.Should().Be(404);
83+
result!.Value.Should().Be(error.DocumentId);
84+
}
85+
}
86+
87+
#endif
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace CSharpFunctionalExtensions.HttpResults.Tests.Utils;
2+
3+
public static class EnumerableExtensions
4+
{
5+
public static async IAsyncEnumerable<T> AsAsyncEnumerable<T>(this IEnumerable<T> items)
6+
{
7+
foreach (var item in items)
8+
yield return item;
9+
10+
await Task.CompletedTask;
11+
}
12+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#if NET10_0_OR_GREATER
2+
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Http.HttpResults;
5+
using Microsoft.Extensions.DependencyInjection;
6+
7+
namespace CSharpFunctionalExtensions.HttpResults.Tests.Utils;
8+
9+
public static class ServerSentEventsExtensions
10+
{
11+
public static async Task<(
12+
HttpResponse Response,
13+
IReadOnlyList<EventMessage> Values
14+
)> ExecuteAndGetResponseAndValues<T>(this ServerSentEventsResult<T> result)
15+
{
16+
var httpContext = new DefaultHttpContext
17+
{
18+
Response = { Body = new MemoryStream() },
19+
RequestServices = new ServiceCollection().BuildServiceProvider(),
20+
};
21+
await result.ExecuteAsync(httpContext);
22+
httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
23+
24+
var body = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
25+
26+
var values = body.Split("\n\n", StringSplitOptions.RemoveEmptyEntries)
27+
.Select(block =>
28+
{
29+
var lines = block.Split('\n', StringSplitOptions.RemoveEmptyEntries);
30+
31+
string? eventType = null;
32+
var data = string.Empty;
33+
34+
foreach (var line in lines)
35+
{
36+
if (line.StartsWith("event: "))
37+
eventType = line["event: ".Length..].Trim();
38+
else if (line.StartsWith("data: "))
39+
data = line["data: ".Length..].Trim();
40+
}
41+
42+
return new EventMessage { Event = eventType, Data = data };
43+
})
44+
.ToList()
45+
.AsReadOnly();
46+
47+
return (httpContext.Response, values);
48+
}
49+
50+
public record EventMessage
51+
{
52+
public string? Event { get; init; }
53+
public string Data { get; init; }
54+
}
55+
}
56+
57+
#endif
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#if NET10_0_OR_GREATER
2+
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Http.HttpResults;
5+
using Microsoft.AspNetCore.Mvc;
6+
7+
namespace CSharpFunctionalExtensions.HttpResults.ResultExtensions;
8+
9+
/// <summary>
10+
/// Extension methods for <see cref="Result{T}" />
11+
/// </summary>
12+
public static partial class ResultExtensions
13+
{
14+
/// <summary>
15+
/// Returns a <see cref="ServerSentEventsResult{T}" /> based of a <see cref="IAsyncEnumerable{T}" /> in case of success
16+
/// result. Returns <see cref="ProblemHttpResult" /> in case of failure. You can override the error status code.
17+
/// </summary>
18+
public static Results<ServerSentEventsResult<T>, ProblemHttpResult> ToServerSentEventsHttpResult<T>(
19+
this Result<IAsyncEnumerable<T>> result,
20+
string? eventType = null,
21+
int failureStatusCode = 400,
22+
Action<ProblemDetails>? customizeProblemDetails = null
23+
)
24+
{
25+
if (result.IsSuccess)
26+
return TypedResults.ServerSentEvents(result.Value, eventType);
27+
28+
var problemDetailsInfo = ProblemDetailsMappingProvider.FindMapping(failureStatusCode);
29+
var problemDetails = new ProblemDetails
30+
{
31+
Status = failureStatusCode,
32+
Title = problemDetailsInfo.Title,
33+
Type = problemDetailsInfo.Type,
34+
Detail = result.Error,
35+
};
36+
37+
customizeProblemDetails?.Invoke(problemDetails);
38+
39+
return TypedResults.Problem(problemDetails);
40+
}
41+
42+
/// <summary>
43+
/// Returns a <see cref="ServerSentEventsResult{T}" /> based of a <see cref="IAsyncEnumerable{T}" /> in case of success
44+
/// result. Returns <see cref="ProblemHttpResult" /> in case of failure. You can override the error status code.
45+
/// </summary>
46+
public static async Task<Results<ServerSentEventsResult<T>, ProblemHttpResult>> ToServerSentEventsHttpResult<T>(
47+
this Task<Result<IAsyncEnumerable<T>>> result,
48+
string? eventType = null,
49+
int failureStatusCode = 400,
50+
Action<ProblemDetails>? customizeProblemDetails = null
51+
)
52+
{
53+
return (await result).ToServerSentEventsHttpResult(eventType, failureStatusCode, customizeProblemDetails);
54+
}
55+
}
56+
#endif

0 commit comments

Comments
 (0)