Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ List<ClassDeclarationSyntax> mapperClasses
new ToStatusCodeHttpResultTE(),
new ToOkHttpResultTE(),
new ToContentHttpResultStringE(),
new ToServerSentEventsHttpResultIAsyncEnumerableTE(),
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace CSharpFunctionalExtensions.HttpResults.Generators.ResultExtensions;

internal class ToServerSentEventsHttpResultIAsyncEnumerableTE : IGenerateMethods
{
public string Generate(string mapperClassName, string resultErrorType, string httpResultType)
{
return $$"""
#if NET10_0_OR_GREATER
/// <summary>
/// Returns a <see cref="ServerSentEventsResult{T}" /> based of a <see cref="IAsyncEnumerable{T}" /> in case of success. Returns custom mapping in case of failure.
/// </summary>
public static Results<ServerSentEventsResult<T>, {{httpResultType}}> ToServerSentEventsHttpResult<T>(this Result<IAsyncEnumerable<T>,{{resultErrorType}}> result, string? eventType = null)
{
if (result.IsSuccess) return TypedResults.ServerSentEvents(result.Value, eventType);

return ErrorMapperInstances.{{mapperClassName}}.Map(result.Error);
}

/// <summary>
/// Returns a <see cref="ServerSentEventsResult{T}" /> based of a <see cref="IAsyncEnumerable{T}" /> in case of success. Returns custom mapping in case of failure.
/// </summary>
public static async Task<Results<ServerSentEventsResult<T>, {{httpResultType}}>> ToServerSentEventsHttpResult<T>(this Task<Result<IAsyncEnumerable<T>,{{resultErrorType}}>> result, string? eventType = null)
{
return (await result).ToServerSentEventsHttpResult(eventType);
}
#endif
""";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#if NET10_0_OR_GREATER

using System.Net.Mime;
using CSharpFunctionalExtensions.HttpResults.ResultExtensions;
using CSharpFunctionalExtensions.HttpResults.Tests.Utils;
using FluentAssertions;
using Microsoft.AspNetCore.Http.HttpResults;

namespace CSharpFunctionalExtensions.HttpResults.Tests.ResultExtensions;

public class ToServerSentEventsHttpResultIAsyncEnumerableT
{
[Fact]
public async Task ResultIAsyncEnumerableT_Success_can_be_mapped_to_ServerSentEventsHttpResult()
{
var testValues = new[] { 42, 420 };
var eventType = "TestEvent";

var asyncEnumerable = testValues.AsAsyncEnumerable();

var result =
Result.Success(asyncEnumerable).ToServerSentEventsHttpResult(eventType).Result as ServerSentEventsResult<int>;

var (response, values) = await result!.ExecuteAndGetResponseAndValues();

result!.StatusCode.Should().Be(200);
response.ContentType.Should().Be(MediaTypeNames.Text.EventStream);
values.Select(v => v.Data).Should().Contain(testValues.Select(v => v.ToString()));
values.Select(v => v.Event).Should().AllBe(eventType);
}

[Fact]
public async Task ResultIAsyncEnumerableT_Success_can_be_mapped_to_ServerSentEventsHttpResult_Async()
{
var testValues = new[] { 42, 420 };
var eventType = "TestEvent";

var asyncEnumerable = testValues.AsAsyncEnumerable();

var result =
(await Task.FromResult(Result.Success(asyncEnumerable)).ToServerSentEventsHttpResult(eventType)).Result
as ServerSentEventsResult<int>;

var (response, values) = await result!.ExecuteAndGetResponseAndValues();

result!.StatusCode.Should().Be(200);
response.ContentType.Should().Be(MediaTypeNames.Text.EventStream);
values.Select(v => v.Data).Should().Contain(testValues.Select(v => v.ToString()));
values.Select(v => v.Event).Should().AllBe(eventType);
}

[Fact]
public void ResultIAsyncEnumerableT_Failure_can_be_mapped_to_ServerSentEventsHttpResult()
{
var error = "Error";

var result =
Result.Failure<IAsyncEnumerable<int>>(error).ToServerSentEventsHttpResult().Result as ProblemHttpResult;

result!.StatusCode.Should().Be(400);
result!.ProblemDetails.Status.Should().Be(400);
result!.ProblemDetails.Detail.Should().Be(error);
}

[Fact]
public async Task ResultIAsyncEnumerableT_Failure_can_be_mapped_to_ServerSentEventsHttpResult_Async()
{
var error = "Error";

var result =
(await Task.FromResult(Result.Failure<IAsyncEnumerable<int>>(error)).ToServerSentEventsHttpResult()).Result
as ProblemHttpResult;

result!.StatusCode.Should().Be(400);
result!.ProblemDetails.Status.Should().Be(400);
result!.ProblemDetails.Detail.Should().Be(error);
}

[Fact]
public void ResultIAsyncEnumerableT_Failure_StatusCode_can_be_changed()
{
var statusCode = 418;
var error = "Error";

var result =
Result.Failure<IAsyncEnumerable<int>>(error).ToServerSentEventsHttpResult(failureStatusCode: statusCode).Result
as ProblemHttpResult;

result!.StatusCode.Should().Be(statusCode);
result!.ProblemDetails.Status.Should().Be(statusCode);
result!.ProblemDetails.Detail.Should().Be(error);
}

[Fact]
public async Task ResultIAsyncEnumerableT_Failure_StatusCode_can_be_changed_Async()
{
var statusCode = 418;
var error = "Error";

var result =
(
await Task.FromResult(Result.Failure<IAsyncEnumerable<int>>(error))
.ToServerSentEventsHttpResult(failureStatusCode: statusCode)
).Result as ProblemHttpResult;

result!.StatusCode.Should().Be(statusCode);
result!.ProblemDetails.Status.Should().Be(statusCode);
result!.ProblemDetails.Detail.Should().Be(error);
}

[Fact]
public void ResultIAsyncEnumerableT_Failure_ProblemDetails_can_be_customized()
{
var error = "Error";
var customTitle = "Custom Title";

var result =
Result
.Failure<IAsyncEnumerable<int>>(error)
.ToServerSentEventsHttpResult(customizeProblemDetails: problemDetails => problemDetails.Title = customTitle)
.Result as ProblemHttpResult;

result!.ProblemDetails.Title.Should().Be(customTitle);
}

[Fact]
public async Task ResultIAsyncEnumerableT_Failure_ProblemDetails_can_be_customized_Async()
{
var error = "Error";
var customTitle = "Custom Title";

var result =
(
await Task.FromResult(Result.Failure<IAsyncEnumerable<int>>(error))
.ToServerSentEventsHttpResult(customizeProblemDetails: problemDetails => problemDetails.Title = customTitle)
).Result as ProblemHttpResult;

result!.ProblemDetails.Title.Should().Be(customTitle);
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#if NET10_0_OR_GREATER

using System.Net.Mime;
using CSharpFunctionalExtensions.HttpResults.ResultExtensions;
using CSharpFunctionalExtensions.HttpResults.Tests.Shared;
using CSharpFunctionalExtensions.HttpResults.Tests.Utils;
using FluentAssertions;
using Microsoft.AspNetCore.Http.HttpResults;

namespace CSharpFunctionalExtensions.HttpResults.Tests.ResultExtensions;

public class ToServerSentEventsHttpResultIAsyncEnumerableTE
{
[Fact]
public async Task ResultIAsyncEnumerableTE_Success_can_be_mapped_to_ServerSentEventsHttpResult()
{
var testValues = new[] { 42, 420 };
var eventType = "TestEvent";

var asyncEnumerable = testValues.AsAsyncEnumerable();

var result =
Result
.Success<IAsyncEnumerable<int>, DocumentMissingError>(asyncEnumerable)
.ToServerSentEventsHttpResult(eventType)
.Result as ServerSentEventsResult<int>;

var (response, values) = await result!.ExecuteAndGetResponseAndValues();

result!.StatusCode.Should().Be(200);
response.ContentType.Should().Be(MediaTypeNames.Text.EventStream);
values.Select(v => v.Data).Should().Contain(testValues.Select(v => v.ToString()));
values.Select(v => v.Event).Should().AllBe(eventType);
}

[Fact]
public async Task ResultIAsyncEnumerableTE_Success_can_be_mapped_to_ServerSentEventsHttpResult_Async()
{
var testValues = new[] { 42, 420 };
var eventType = "TestEvent";

var asyncEnumerable = testValues.AsAsyncEnumerable();

var result =
(
await Task.FromResult(Result.Success<IAsyncEnumerable<int>, DocumentMissingError>(asyncEnumerable))
.ToServerSentEventsHttpResult(eventType)
).Result as ServerSentEventsResult<int>;

var (response, values) = await result!.ExecuteAndGetResponseAndValues();

result!.StatusCode.Should().Be(200);
response.ContentType.Should().Be(MediaTypeNames.Text.EventStream);
values.Select(v => v.Data).Should().Contain(testValues.Select(v => v.ToString()));
values.Select(v => v.Event).Should().AllBe(eventType);
}

[Fact]
public void ResultIAsyncEnumerableTE_Failure_can_be_mapped_to_ServerSentEventsHttpResult()
{
var error = new DocumentMissingError { DocumentId = Guid.NewGuid().ToString() };

var result =
Result.Failure<IAsyncEnumerable<int>, DocumentMissingError>(error).ToServerSentEventsHttpResult().Result
as NotFound<string>;

result!.StatusCode.Should().Be(404);
result!.Value.Should().Be(error.DocumentId);
}

[Fact]
public async Task ResultIAsyncEnumerableTE_Failure_can_be_mapped_to_ServerSentEventsHttpResult_Async()
{
var error = new DocumentMissingError { DocumentId = Guid.NewGuid().ToString() };

var result =
(
await Task.FromResult(Result.Failure<IAsyncEnumerable<int>, DocumentMissingError>(error))
.ToServerSentEventsHttpResult()
).Result as NotFound<string>;

result!.StatusCode.Should().Be(404);
result!.Value.Should().Be(error.DocumentId);
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace CSharpFunctionalExtensions.HttpResults.Tests.Utils;

public static class EnumerableExtensions
{
public static async IAsyncEnumerable<T> AsAsyncEnumerable<T>(this IEnumerable<T> items)
{
foreach (var item in items)
yield return item;

await Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#if NET10_0_OR_GREATER

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Extensions.DependencyInjection;

namespace CSharpFunctionalExtensions.HttpResults.Tests.Utils;

public static class ServerSentEventsExtensions
{
public static async Task<(
HttpResponse Response,
IReadOnlyList<EventMessage> Values
)> ExecuteAndGetResponseAndValues<T>(this ServerSentEventsResult<T> result)
{
var httpContext = new DefaultHttpContext
{
Response = { Body = new MemoryStream() },
RequestServices = new ServiceCollection().BuildServiceProvider(),
};
await result.ExecuteAsync(httpContext);
httpContext.Response.Body.Seek(0, SeekOrigin.Begin);

var body = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();

var values = body.Split("\n\n", StringSplitOptions.RemoveEmptyEntries)
.Select(block =>
{
var lines = block.Split('\n', StringSplitOptions.RemoveEmptyEntries);

string? eventType = null;
var data = string.Empty;

foreach (var line in lines)
{
if (line.StartsWith("event: "))
eventType = line["event: ".Length..].Trim();
else if (line.StartsWith("data: "))
data = line["data: ".Length..].Trim();
}

return new EventMessage { Event = eventType, Data = data };
})
.ToList()
.AsReadOnly();

return (httpContext.Response, values);
}

public record EventMessage
{
public string? Event { get; init; }
public string Data { get; init; }

Check warning on line 53 in CSharpFunctionalExtensions.HttpResults.Tests/Utils/ServerSentEventsExtensions.cs

View workflow job for this annotation

GitHub Actions / test-versions (2.29.0) / test-version

Non-nullable property 'Data' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 53 in CSharpFunctionalExtensions.HttpResults.Tests/Utils/ServerSentEventsExtensions.cs

View workflow job for this annotation

GitHub Actions / test-versions (2.29.0) / test-version

Non-nullable property 'Data' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 53 in CSharpFunctionalExtensions.HttpResults.Tests/Utils/ServerSentEventsExtensions.cs

View workflow job for this annotation

GitHub Actions / test-versions (2.*) / test-version

Non-nullable property 'Data' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 53 in CSharpFunctionalExtensions.HttpResults.Tests/Utils/ServerSentEventsExtensions.cs

View workflow job for this annotation

GitHub Actions / test-versions (2.*) / test-version

Non-nullable property 'Data' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 53 in CSharpFunctionalExtensions.HttpResults.Tests/Utils/ServerSentEventsExtensions.cs

View workflow job for this annotation

GitHub Actions / test-versions (3.0.0) / test-version

Non-nullable property 'Data' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 53 in CSharpFunctionalExtensions.HttpResults.Tests/Utils/ServerSentEventsExtensions.cs

View workflow job for this annotation

GitHub Actions / test-versions (3.0.0) / test-version

Non-nullable property 'Data' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 53 in CSharpFunctionalExtensions.HttpResults.Tests/Utils/ServerSentEventsExtensions.cs

View workflow job for this annotation

GitHub Actions / test-versions (3.*) / test-version

Non-nullable property 'Data' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 53 in CSharpFunctionalExtensions.HttpResults.Tests/Utils/ServerSentEventsExtensions.cs

View workflow job for this annotation

GitHub Actions / test-versions (3.*) / test-version

Non-nullable property 'Data' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 53 in CSharpFunctionalExtensions.HttpResults.Tests/Utils/ServerSentEventsExtensions.cs

View workflow job for this annotation

GitHub Actions / check

Non-nullable property 'Data' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 53 in CSharpFunctionalExtensions.HttpResults.Tests/Utils/ServerSentEventsExtensions.cs

View workflow job for this annotation

GitHub Actions / check

Non-nullable property 'Data' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#if NET10_0_OR_GREATER

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

namespace CSharpFunctionalExtensions.HttpResults.ResultExtensions;

/// <summary>
/// Extension methods for <see cref="Result{T}" />
/// </summary>
public static partial class ResultExtensions
{
/// <summary>
/// Returns a <see cref="ServerSentEventsResult{T}" /> based of a <see cref="IAsyncEnumerable{T}" /> in case of success
/// result. Returns <see cref="ProblemHttpResult" /> in case of failure. You can override the error status code.
/// </summary>
public static Results<ServerSentEventsResult<T>, ProblemHttpResult> ToServerSentEventsHttpResult<T>(
this Result<IAsyncEnumerable<T>> result,
string? eventType = null,
int failureStatusCode = 400,
Action<ProblemDetails>? customizeProblemDetails = null
)
{
if (result.IsSuccess)
return TypedResults.ServerSentEvents(result.Value, eventType);

var problemDetailsInfo = ProblemDetailsMappingProvider.FindMapping(failureStatusCode);
var problemDetails = new ProblemDetails
{
Status = failureStatusCode,
Title = problemDetailsInfo.Title,
Type = problemDetailsInfo.Type,
Detail = result.Error,
};

customizeProblemDetails?.Invoke(problemDetails);

return TypedResults.Problem(problemDetails);
}

/// <summary>
/// Returns a <see cref="ServerSentEventsResult{T}" /> based of a <see cref="IAsyncEnumerable{T}" /> in case of success
/// result. Returns <see cref="ProblemHttpResult" /> in case of failure. You can override the error status code.
/// </summary>
public static async Task<Results<ServerSentEventsResult<T>, ProblemHttpResult>> ToServerSentEventsHttpResult<T>(
this Task<Result<IAsyncEnumerable<T>>> result,
string? eventType = null,
int failureStatusCode = 400,
Action<ProblemDetails>? customizeProblemDetails = null
)
{
return (await result).ToServerSentEventsHttpResult(eventType, failureStatusCode, customizeProblemDetails);
}
}
#endif
Loading
Loading