Skip to content

Instantly share code, notes, and snippets.

@dj-nitehawk
Last active October 1, 2024 09:00
Show Gist options
  • Save dj-nitehawk/66cba78a1a3a1e0799d87d67d8aa14bd to your computer and use it in GitHub Desktop.
Save dj-nitehawk/66cba78a1a3a1e0799d87d67d8aa14bd to your computer and use it in GitHub Desktop.
Response sending post-processor with ErrorOr package
var bld = WebApplication.CreateBuilder(args);
bld.Services
.SwaggerDocument()
.AddFastEndpoints();
var app = bld.Build();
app.UseFastEndpoints(
c =>
{
c.Errors.UseProblemDetails();
c.Endpoints.Configurator =
ep =>
{
if (ep.ResDtoType.IsAssignableTo(typeof(IErrorOr)))
{
ep.DontAutoSendResponse();
ep.PostProcessor<ResponseSender>(Order.After);
ep.Description(
b => b.ClearDefaultProduces()
.Produces(200, ep.ResDtoType.GetGenericArguments()[0])
.ProducesProblemDetails());
}
};
})
.UseSwaggerGen();
app.Run();
sealed class Request
{
public bool IsHappyPath { get; set; }
}
sealed class Response
{
public string Message { get; set; }
}
sealed class TestEndpoint : Endpoint<Request, ErrorOr<Response>> //set response type to ErrorOr<T>
{
public override void Configure()
{
Get("test/{IsHappyPath}");
AllowAnonymous();
}
public override Task<ErrorOr<Response>> ExecuteAsync(Request r, CancellationToken ct)
=> Task.FromResult(HelloService.SayHello(r.IsHappyPath)); //return a ErrorOr<T>
}
sealed class HelloService
{
public static ErrorOr<Response> SayHello(bool isHappyPath)
{
if (!isHappyPath)
return Error.Validation("isHappyPath", "You have chosen to be unhappy!");
return new Response { Message = "You have chosen to be happy!" };
}
}
sealed class ResponseSender : IGlobalPostProcessor
{
public Task PostProcessAsync(IPostProcessorContext ctx, CancellationToken ct)
{
if (ctx.HttpContext.ResponseStarted() || ctx.Response is not IErrorOr errorOr)
return Task.CompletedTask;
if (!errorOr.IsError)
return ctx.HttpContext.Response.SendAsync(GetValueFromErrorOr(errorOr), cancellation: ct);
if (errorOr.Errors?.All(e => e.Type == ErrorType.Validation) is true)
{
return ctx.HttpContext.Response.SendErrorsAsync(
failures: [..errorOr.Errors.Select(e => new ValidationFailure(e.Code, e.Description))],
cancellation: ct);
}
var problem = errorOr.Errors?.FirstOrDefault(e => e.Type != ErrorType.Validation);
switch (problem?.Type)
{
case ErrorType.Conflict:
return ctx.HttpContext.Response.SendAsync("Duplicate submission!", 409, cancellation: ct);
case ErrorType.NotFound:
return ctx.HttpContext.Response.SendNotFoundAsync(ct);
case ErrorType.Unauthorized:
return ctx.HttpContext.Response.SendUnauthorizedAsync(ct);
case ErrorType.Forbidden:
return ctx.HttpContext.Response.SendForbiddenAsync(ct);
case null:
throw new InvalidOperationException();
}
return Task.CompletedTask;
}
//cached compiled expressions for reading ErrorOr<T>.Value property
static readonly ConcurrentDictionary<Type, Func<object, object>> _valueAccessors = new();
static object GetValueFromErrorOr(object errorOr)
{
ArgumentNullException.ThrowIfNull(errorOr);
var tErrorOr = errorOr.GetType();
if (!tErrorOr.IsGenericType || tErrorOr.GetGenericTypeDefinition() != typeof(ErrorOr<>))
throw new InvalidOperationException("The provided object is not an instance of ErrorOr<>.");
return _valueAccessors.GetOrAdd(tErrorOr, CreateValueAccessor)(errorOr);
static Func<object, object> CreateValueAccessor(Type errorOrType)
{
var parameter = Expression.Parameter(typeof(object), "errorOr");
return Expression.Lambda<Func<object, object>>(
Expression.Convert(
Expression.Property(
Expression.Convert(parameter, errorOrType),
"Value"),
typeof(object)),
parameter)
.Compile();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment