Skip to content

Commit

Permalink
Ability to use IEndpointMetadataProvider on resource types, and for a…
Browse files Browse the repository at this point in the history
… 201 status code convention from the CreationResponse base class. Closes GH-311. Closes GH-304
  • Loading branch information
Jeremy D. Miller authored and Jeremy D. Miller committed Apr 19, 2023
1 parent 8c14775 commit 39337d8
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using JasperFx.Core;
using Microsoft.AspNetCore.Http.Metadata;
using Shouldly;
using WolverineWebApi;

namespace Wolverine.Http.Tests;

public class using_create_response_and_metadata_derived_from_response_type : IntegrationContext
{
public using_create_response_and_metadata_derived_from_response_type(AppFixture fixture) : base(fixture)
{
}


[Fact]
public void read_metadata_from_IEndpointMetadataProvider()
{
var chain = HttpChain.ChainFor<CreateEndpoint>(x => x.Create(null));

var endpoint = chain.BuildEndpoint();

// Should remove the 200 OK response
endpoint
.Metadata
.OfType<IProducesResponseTypeMetadata>()
.Any(x => x.StatusCode == 200)
.ShouldBeFalse();

var responseMetadata = endpoint
.Metadata
.OfType<IProducesResponseTypeMetadata>()
.FirstOrDefault(x => x.StatusCode == 201);

responseMetadata.ShouldNotBeNull();
responseMetadata.Type.ShouldBe(typeof(IssueCreated));
}

[Fact]
public async Task make_the_request()
{
await Store.Advanced.Clean.DeleteDocumentsByTypeAsync(typeof(Issue));

var result = await Scenario(x =>
{
x.Post.Json(new CreateIssue("It's bad")).ToUrl("/issue");
x.StatusCodeShouldBe(201);
});

var created = result.ReadAsJson<IssueCreated>();
created.ShouldNotBeNull();

using var session = Store.LightweightSession();
var issue = await session.LoadAsync<Issue>(created.Id);
issue.ShouldNotBeNull();
issue.Title.ShouldBe("It's bad");

result.Context.Response.Headers.Location.Single().ShouldBe("/issue/" + created.Id);
}
}
20 changes: 20 additions & 0 deletions src/Http/Wolverine.Http/HttpChain.EndpointBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Reflection;
using JasperFx.Core.Reflection;
using JasperFx.RuntimeCompiler;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Routing;

namespace Wolverine.Http;
Expand Down Expand Up @@ -31,14 +34,31 @@ public RouteEndpoint BuildEndpoint()
{
DisplayName = DisplayName
};


foreach (var configuration in _builderConfigurations)
{
configuration(builder);
}

if (ResourceType != null && ResourceType.CanBeCastTo(typeof(IEndpointMetadataProvider)))
{
var applier = typeof(Applier<>).CloseAndBuildAs<IApplier>(ResourceType);
applier.Apply(builder, Method.Method);
}

Endpoint = (RouteEndpoint?)builder.Build();

return Endpoint;
}

internal interface IApplier
{
void Apply(EndpointBuilder builder, MethodInfo method);
}

internal class Applier<T> : IApplier where T : IEndpointMetadataProvider
{
public void Apply(EndpointBuilder builder, MethodInfo method) => T.PopulateMetadata(method, builder);
}
}
74 changes: 74 additions & 0 deletions src/Http/Wolverine.Http/IHttpAware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.Reflection;
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;
using JasperFx.CodeGeneration.Model;
using JasperFx.Core;
using JasperFx.Core.Reflection;
using Lamar;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Metadata;

namespace Wolverine.Http;

/// <summary>
/// Interface for resource types in Wolverine.Http that need to modify
/// how the HTTP response is formatted. Use this for additional headers
/// or customized status codes
/// </summary>
public interface IHttpAware : IEndpointMetadataProvider
{
void Apply(HttpContext context);
}

internal class HttpAwarePolicy : IHttpPolicy
{
public void Apply(IReadOnlyList<HttpChain> chains, GenerationRules rules, IContainer container)
{
var matching = chains.Where(x => x.ResourceType != null && x.ResourceType.CanBeCastTo(typeof(IHttpAware)));
foreach (var chain in matching)
{
var resource = chain.Method.Creates.FirstOrDefault(x => x.VariableType == chain.ResourceType);
if (resource == null) return;

var apply = new MethodCall(typeof(IHttpAware), nameof(IHttpAware.Apply))
{
Target = new CastVariable(resource, typeof(IHttpAware))
};

// This will have to run before any kind of resource writing
chain.Postprocessors.Insert(0, apply);
}
}
}

/// <summary>
/// Base class for resource types that denote some kind of resource being created
/// in the system. Wolverine specific, and more efficient, version of Created<T> from ASP.Net Core
/// </summary>
public abstract record CreationResponse : IHttpAware
{
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.RemoveAll(x => x is IProducesResponseTypeMetadata m && m.StatusCode == 200);

var create = new MethodCall(method.DeclaringType, method).Creates.FirstOrDefault()?.VariableType;
var metadata = new Metadata { Type = create, StatusCode = 201 };
builder.Metadata.Add(metadata);
}

protected virtual string Url() => string.Empty;

void IHttpAware.Apply(HttpContext context)
{
context.Response.Headers.Location = Url();
context.Response.StatusCode = 201;
}

internal class Metadata : IProducesResponseTypeMetadata
{
public Type? Type { get; init; }
public int StatusCode { get; init; }
public IEnumerable<string> ContentTypes => new string[] { "application/json" };
}
}
2 changes: 1 addition & 1 deletion src/Http/Wolverine.Http/Wolverine.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<TargetFrameworks>net7.0</TargetFrameworks>
<DebugType>portable</DebugType>
<PackageId>WolverineFx.Http</PackageId>
<LangVersion>latest</LangVersion>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<OutputType>library</OutputType>
</PropertyGroup>
Expand Down
5 changes: 5 additions & 0 deletions src/Http/Wolverine.Http/WolverineHttpOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ namespace Wolverine.Http;
[Singleton]
public class WolverineHttpOptions
{
public WolverineHttpOptions()
{
Policies.Add(new HttpAwarePolicy());
}

internal JsonSerializerOptions JsonSerializerOptions { get; set; } = new();
internal HttpGraph? Endpoints { get; set; }

Expand Down
36 changes: 36 additions & 0 deletions src/Http/WolverineWebApi/CreateEndpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Marten.Schema.Identity;
using Wolverine.Http;
using Wolverine.Marten;

namespace WolverineWebApi;

public class CreateEndpoint
{
[WolverinePost("/issue")]
public (IssueCreated, InsertDoc<Issue>) Create(CreateIssue command)
{
var id = CombGuidIdGeneration.NewGuid();
var issue = new Issue
{
Id = id, Title = command.Title
};

return (new IssueCreated(id), MartenOps.Insert(issue));
}
}

public record CreateIssue(string Title);

public record IssueCreated(Guid Id) : CreationResponse
{
protected override string Url()
{
return "/issue/" + Id;
}
}

public class Issue
{
public Guid Id { get; set; }
public string Title { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// <auto-generated/>
#pragma warning disable
using Microsoft.AspNetCore.Routing;
using System;
using System.Linq;
using Wolverine.Http;
using Wolverine.Marten.Publishing;
using Wolverine.Runtime;

namespace Internal.Generated.WolverineHandlers
{
// START: POST_issue
public class POST_issue : Wolverine.Http.HttpHandler
{
private readonly Wolverine.Http.WolverineHttpOptions _options;
private readonly Wolverine.Marten.Publishing.OutboxedSessionFactory _outboxedSessionFactory;
private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime;

public POST_issue(Wolverine.Http.WolverineHttpOptions options, Wolverine.Marten.Publishing.OutboxedSessionFactory outboxedSessionFactory, Wolverine.Runtime.IWolverineRuntime wolverineRuntime) : base(options)
{
_options = options;
_outboxedSessionFactory = outboxedSessionFactory;
_wolverineRuntime = wolverineRuntime;
}



public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext)
{
var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime);
await using var documentSession = _outboxedSessionFactory.OpenSession(messageContext);
var createEndpoint = new WolverineWebApi.CreateEndpoint();
var (command, jsonContinue) = await ReadJsonAsync<WolverineWebApi.CreateIssue>(httpContext);
if (jsonContinue == Wolverine.HandlerContinuation.Stop) return;
(var issueCreated, var insertDoc) = createEndpoint.Create(command);

// Outgoing, cascaded message
await messageContext.EnqueueCascadingAsync(insertDoc).ConfigureAwait(false);

((Wolverine.Http.IHttpAware)issueCreated).Apply(httpContext);

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

await WriteJsonAsync(httpContext, issueCreated);

// Commit the unit of work
await documentSession.SaveChangesAsync(httpContext.RequestAborted).ConfigureAwait(false);
}

}

// END: POST_issue


}

0 comments on commit 39337d8

Please sign in to comment.