Skip to content

Commit

Permalink
"Required" entity loading in HTTP mechanics Closes GH-470
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremydmiller committed Jul 14, 2023
1 parent a59f3e9 commit 8d7943b
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 1 deletion.
10 changes: 10 additions & 0 deletions src/Http/Wolverine.Http.Tests/todo_endpoint_specs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,14 @@ await Scenario(opts =>
changes.IsComplete.ShouldBeTrue();
changes.Name.ShouldBe("Second");
}

[Fact]
public async Task get_an_automatic_404_when_related_entity_does_not_exist()
{
await Scenario(opts =>
{
opts.Put.Json(new UpdateRequest("Second", true)).ToUrl("/todos/1222222222");
opts.StatusCodeShouldBe(404);
});
}
}
75 changes: 75 additions & 0 deletions src/Http/Wolverine.Http/Policies/RequiredEntityPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.ComponentModel.DataAnnotations;
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;
using JasperFx.CodeGeneration.Model;
using JasperFx.Core.Reflection;
using Lamar;
using Microsoft.AspNetCore.Http;

namespace Wolverine.Http.Policies;

internal class RequiredEntityPolicy : IHttpPolicy
{
public void Apply(IReadOnlyList<HttpChain> chains, GenerationRules rules, IContainer container)
{
foreach (var chain in chains)
{
if (chain.RoutePattern.Parameters.Any())
{
var requiredParameters = chain.Method.Method.GetParameters()
.Where(x => x.HasAttribute<RequiredAttribute>() && x.ParameterType.IsClass).ToArray();

if (requiredParameters.Any())
{
chain.Metadata.Produces(404);

foreach (var parameter in requiredParameters)
{
var frame = new SetStatusCodeAndReturnFrame(parameter.ParameterType);
chain.Middleware.Add(frame);
}
}
}
}
}
}

internal class SetStatusCodeAndReturnFrame : SyncFrame
{
private readonly Type _entityType;
private Variable _httpResponse;
private Variable _entity;

public SetStatusCodeAndReturnFrame(Type entityType)
{
_entityType = entityType;
}

public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
{
writer.WriteComment("404 if this required object is null");
writer.Write($"BLOCK:if ({_entity.Usage} == null)");
writer.Write($"{_httpResponse.Usage}.{nameof(HttpResponse.StatusCode)} = 404;");
if (method.AsyncMode == AsyncMode.ReturnCompletedTask)
{
writer.Write($"return {typeof(Task).FullNameInCode()}.{nameof(Task.CompletedTask)};");
}
else
{
writer.Write("return;");
}

writer.FinishBlock();

Next?.GenerateCode(method, writer);
}

public override IEnumerable<Variable> FindVariables(IMethodVariables chain)
{
_entity = chain.FindVariable(_entityType);
yield return _entity;

_httpResponse = chain.FindVariable(typeof(HttpResponse));
yield return _httpResponse;
}
}
2 changes: 2 additions & 0 deletions src/Http/Wolverine.Http/WolverineHttpOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Lamar;
using Wolverine.Configuration;
using Wolverine.Http.CodeGen;
using Wolverine.Http.Policies;
using Wolverine.Http.Runtime;
using Wolverine.Middleware;

Expand All @@ -16,6 +17,7 @@ public WolverineHttpOptions()
{
Policies.Add(new HttpAwarePolicy());
Policies.Add(new RequestIdPolicy());
Policies.Add(new RequiredEntityPolicy());
}

internal JsonSerializerOptions JsonSerializerOptions { get; set; } = new();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// <auto-generated/>
#pragma warning disable
using Marten;
using Microsoft.AspNetCore.Routing;
using System;
using System.Linq;
using Wolverine.Http;

namespace Internal.Generated.WolverineHandlers
{
// START: PUT_todos_id
public class PUT_todos_id : Wolverine.Http.HttpHandler
{
private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;
private readonly Marten.ISessionFactory _sessionFactory;

public PUT_todos_id(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Marten.ISessionFactory sessionFactory) : base(wolverineHttpOptions)
{
_wolverineHttpOptions = wolverineHttpOptions;
_sessionFactory = sessionFactory;
}



public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext)
{
await using var documentSession = _sessionFactory.OpenSession();
if (!int.TryParse((string)httpContext.GetRouteValue("id"), out var id))
{
httpContext.Response.StatusCode = 404;
return;
}


var (request, jsonContinue) = await ReadJsonAsync<WolverineWebApi.Samples.UpdateRequest>(httpContext);
if (jsonContinue == Wolverine.HandlerContinuation.Stop) return;
var todo = await WolverineWebApi.Samples.UpdateEndpoint.LoadAsync(id, documentSession).ConfigureAwait(false);
// 404 if this required object is null
if (todo == null)
{
httpContext.Response.StatusCode = 404;
return;
}

var storeDoc = WolverineWebApi.Samples.UpdateEndpoint.Put(id, request, todo);

// Placed by Wolverine's ISideEffect policy
storeDoc.Execute(documentSession);


// Commit any outstanding Marten changes
await documentSession.SaveChangesAsync(httpContext.RequestAborted).ConfigureAwait(false);

// Wolverine automatically sets the status code to 204 for empty responses
httpContext.Response.StatusCode = 204;
}

}

// END: PUT_todos_id


}

3 changes: 2 additions & 1 deletion src/Http/WolverineWebApi/Samples/TodoController.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Marten;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
Expand Down Expand Up @@ -127,7 +128,7 @@ public static class UpdateEndpoint
=> session.LoadAsync<Todo>(id);

[WolverinePut("/todos/{id:int}"), EmptyResponse]
public static StoreDoc<Todo> Put(int id, UpdateRequest request, Todo todo)
public static StoreDoc<Todo> Put(int id, UpdateRequest request, [Required] Todo? todo)
{
todo.Name = request.Name;
todo.IsComplete = request.IsComplete;
Expand Down

0 comments on commit 8d7943b

Please sign in to comment.