Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: IResourceWriterPolicy accessible and CompiledQueryResourceWriter #687

Merged
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
45 changes: 42 additions & 3 deletions docs/guide/http/marten.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ look like this:
return Results.Ok(invoice);
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Documents.cs#L11-L28' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_get_invoice_longhand' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Documents.cs#L13-L30' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_get_invoice_longhand' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Pretty straightforward, but it's a little annoying to have to scatter in all the attributes for OpenAPI and there's definitely
Expand All @@ -49,7 +49,7 @@ public static Invoice Get([Document] Invoice invoice)
return invoice;
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Documents.cs#L30-L38' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_document_attribute' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Documents.cs#L32-L40' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_document_attribute' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Notice that the `[Document]` attribute was able to use the "id" route parameter. By default, Wolverine is looking first
Expand All @@ -66,7 +66,7 @@ public static IMartenOp Approve([Document("number")] Invoice invoice)
return MartenOps.Store(invoice);
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Documents.cs#L47-L56' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_overriding_route_argument_with_document_attribute' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Documents.cs#L49-L58' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_overriding_route_argument_with_document_attribute' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


Expand Down Expand Up @@ -253,3 +253,42 @@ public static (OrderStatus, Events) Post(MarkItemReady command, Order order)
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Orders.cs#L193-L223' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_returning_multiple_events_from_http_endpoint' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

### Compiled Query Resource Writer Policy

Marten integration comes with an `IResourceWriterPolicy` policy that handles compiled queries as return types.
Register it in `WolverineHttpOptions` like this:

<!-- snippet: sample_user_marten_compiled_query_policy -->
<a id='snippet-sample_user_marten_compiled_query_policy'></a>
```cs
opts.UseMartenCompiledQueryResultPolicy();
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L144-L146' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_user_marten_compiled_query_policy' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

If you now return a compiled query from an Endpoint the result will get directly streamed to the client as JSON. Short circuiting JSON deserialization.
<!-- snippet: sample_compiled_query_return_endpoint -->
<a id='snippet-sample_compiled_query_return_endpoint'></a>
```cs
[WolverineGet("/invoices/approved")]
public static ApprovedInvoicedCompiledQuery GetApproved()
{
return new ApprovedInvoicedCompiledQuery();
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Documents.cs#L60-L66' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_compiled_query_return_endpoint' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

<!-- snippet: sample_compiled_query_return_query -->
<a id='snippet-sample_compiled_query_return_query'></a>
```cs
public class ApprovedInvoicedCompiledQuery : ICompiledListQuery<Invoice>
{
public Expression<Func<IMartenQueryable<Invoice>, IEnumerable<Invoice>>> QueryIs()
{
return q => q.Where(x => x.Approved);
}
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Documents.cs#L82-L92' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_compiled_query_return_query' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
41 changes: 39 additions & 2 deletions docs/guide/http/policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ app.MapWolverineEndpoints(opts =>
// Wolverine.Http.FluentValidation
opts.UseFluentValidationProblemDetailMiddleware();
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L116-L137' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_configure_endpoints' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L117-L138' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_configure_endpoints' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

The `HttpChain` model is a configuration time structure that Wolverine.Http will use at runtime to create the full
Expand Down Expand Up @@ -97,5 +97,42 @@ app.MapWolverineEndpoints(opts =>
// Wolverine.Http.FluentValidation
opts.UseFluentValidationProblemDetailMiddleware();
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L116-L137' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_configure_endpoints' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L117-L138' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_configure_endpoints' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Resource Writer Policies

Wolverine has an additional type of policy that deals with how an endpoints primary result is handled.

<!-- snippet: sample_IResourceWriterPolicy -->
<a id='snippet-sample_iresourcewriterpolicy'></a>
```cs
/// <summary>
/// Use to apply custom handling to the primary result of an HTTP endpoint handler
/// </summary>
public interface IResourceWriterPolicy
{
/// <summary>
/// Called during bootstrapping to see whether this policy can handle the chain. If yes no further policies are tried.
/// </summary>
/// <param name="chain"> The chain to test against</param>
/// <returns>True if it applies to the chain, false otherwise</returns>
bool TryApply(HttpChain chain);
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/Wolverine.Http/Resources/IResourceWriterPolicy.cs#L3-L17' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_iresourcewriterpolicy' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Only one of these so called resource writer policies can apply to each endpoint and there are a couple of built in policies already.

If you need special handling of a primary return type you can implement `IResourceWriterPolicy` and register it in `WolverineHttpOptions`

<!-- snippet: sample_register_resource_writer_policy -->
<a id='snippet-sample_register_resource_writer_policy'></a>
```cs
opts.AddResourceWriterPolicy<CustomResourceWriterPolicy>();
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L153-L155' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_register_resource_writer_policy' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Resource writer policies registered this way will be applied in order before all built in policies.
95 changes: 95 additions & 0 deletions src/Http/Wolverine.Http.Marten/CompiledQueryWriterPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;
using JasperFx.CodeGeneration.Model;
using Marten;
using Marten.Linq;
using Microsoft.AspNetCore.Http;
using Wolverine.Http.Resources;

namespace Wolverine.Http.Marten;

#nullable enable

public class CompiledQueryWriterPolicy : IResourceWriterPolicy
{
public bool TryApply(HttpChain chain)
{
bool ImplementsGenericInterface(Type type, Type assigned)
{
return type.GetInterfaces().Any(x =>
x.IsGenericType && x.GetGenericTypeDefinition() == assigned);
}

var result = chain.Method.Creates.FirstOrDefault();
if (result is null) return false;
if (ImplementsGenericInterface(result.VariableType, typeof(ICompiledListQuery<,>)))
{
chain.Postprocessors.Add(new MartenWriteArrayCodeFrame(result));
return true;
}

if (ImplementsGenericInterface(result.VariableType, typeof(ICompiledQuery<,>)))
{
chain.Postprocessors.Add(new MartenWriteJsonCodeFrame(result));
return true;
}

return false;
}
}

public class MartenWriteJsonCodeFrame : AsyncFrame
{
private Variable? _documentSession;
private Variable? _httpContext;
private readonly Variable _compiledQuery;

public MartenWriteJsonCodeFrame(Variable variable)
{
_compiledQuery = variable;
}

public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
{
writer.WriteComment("Run the compiled query and stream the response");
writer.Write($"await Marten.AspNetCore.QueryableExtensions.WriteOne({_documentSession?.Usage}, {_compiledQuery.Usage}, {_httpContext?.Usage});");
Next?.GenerateCode(method, writer);
}
public override IEnumerable<Variable> FindVariables(IMethodVariables chain)
{
_documentSession = chain.FindVariable(typeof(IDocumentSession));
yield return _documentSession;
_httpContext = chain.FindVariable(typeof(HttpContext));
yield return _httpContext;
foreach (var variable in base.FindVariables(chain)) yield return variable;
}
}

public class MartenWriteArrayCodeFrame : AsyncFrame
{
private Variable? _documentSession;
private Variable? _httpContext;
private readonly Variable _compiledQuery;

public MartenWriteArrayCodeFrame(Variable variable)
{
_compiledQuery = variable;
}

public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
{
writer.WriteComment("Run the compiled query and stream the response");
writer.Write($"await Marten.AspNetCore.QueryableExtensions.WriteArray({_documentSession?.Usage}, {_compiledQuery.Usage}, {_httpContext?.Usage});");
Next?.GenerateCode(method, writer);
}
public override IEnumerable<Variable> FindVariables(IMethodVariables chain)
{
_documentSession = chain.FindVariable(typeof(IDocumentSession));
yield return _documentSession;
_httpContext = chain.FindVariable(typeof(HttpContext));
yield return _httpContext;
foreach (var variable in base.FindVariables(chain)) yield return variable;
}
}

#nullable disable
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Persistence\Wolverine.Marten\Wolverine.Marten.csproj" />
<ProjectReference Include="..\Wolverine.Http\Wolverine.Http.csproj" />
<PackageReference Include="Marten.AspNetCore" Version="6.4.0" />
</ItemGroup>

</Project>
13 changes: 13 additions & 0 deletions src/Http/Wolverine.Http.Marten/WolverineHttpOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Wolverine.Http.Marten;

public static class WolverineHttpOptionsExtensions
{
/// <summary>
/// Adds an <see cref="IResourceWriterPolicy"/> that streams <see cref="ICompiledQuery"/>
/// </summary>
/// <param name="options">Options to apply policy on</param>
public static void UseMartenCompiledQueryResultPolicy(this WolverineHttpOptions options)
{
options.AddResourceWriterPolicy<CompiledQueryWriterPolicy>();
}
}
69 changes: 69 additions & 0 deletions src/Http/Wolverine.Http.Tests/Marten/compiled_query_writer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Alba;
using Marten.Schema.Identity;
using Shouldly;
using WolverineWebApi.Marten;

namespace Wolverine.Http.Tests.Marten;

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

[Fact]
public async Task endpoint_returning_compiled_list_query_should_return_query_result()
{
await using var session = Store.LightweightSession();
int notApprovedInvoices = 5;
int approvedInvoices = 3;
for (int i = 0; i < notApprovedInvoices; i++)
{
var invoice =
new Invoice()
{
Approved = false
};
session.Store(invoice);
}

for (int i = 0; i < approvedInvoices; i++)
{
var invoice =
new Invoice()
{
Approved = true
};
session.Store(invoice);
}

await session.SaveChangesAsync();

var approvedInvoiceList = await Host.GetAsJson<List<Invoice>>("/invoices/approved");
approvedInvoiceList.ShouldNotBeNull();
approvedInvoiceList.Count.ShouldBe(approvedInvoices);
}

[Fact]
public async Task endpoint_returning_compiled_query_should_return_query_result()
{
var invoice = new Invoice()
{
Id = Guid.NewGuid()
};
using var session = Store.LightweightSession();
session.Store(invoice);
await session.SaveChangesAsync();


var invoiceCompiled = await Host.GetAsJson<Invoice>($"/invoices/compiled/{invoice.Id}");
invoiceCompiled.ShouldNotBeNull();
invoiceCompiled.Id.ShouldBe(invoice.Id);

await Host.Scenario(x =>
{
x.Get.Url($"/invoices/compiled/{Guid.NewGuid()}");
x.StatusCodeShouldBe(404);
});
}
}
8 changes: 5 additions & 3 deletions src/Http/Wolverine.Http/HttpGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public partial class HttpGraph : EndpointDataSource, ICodeFileCollection, IChang
private readonly List<RouteEndpoint> _endpoints = new();
private readonly WolverineOptions _options;

private readonly List<IResourceWriterPolicy> _writerPolicies = new()
private readonly List<IResourceWriterPolicy> _builtInWriterPolicies = new()
{
new EmptyBody204Policy(),
new StatusCodePolicy(),
Expand All @@ -33,6 +33,7 @@ public partial class HttpGraph : EndpointDataSource, ICodeFileCollection, IChang
new JsonResourceWriterPolicy()
};

private readonly List<IResourceWriterPolicy> _optionsWriterPolicies = new();

public HttpGraph(WolverineOptions options, IContainer container)
{
Expand All @@ -45,7 +46,7 @@ public HttpGraph(WolverineOptions options, IContainer container)

internal IContainer Container { get; }

internal IEnumerable<IResourceWriterPolicy> WriterPolicies => _writerPolicies;
internal IEnumerable<IResourceWriterPolicy> WriterPolicies => _optionsWriterPolicies.Concat(_builtInWriterPolicies);

public override IReadOnlyList<Endpoint> Endpoints => _endpoints;

Expand Down Expand Up @@ -107,6 +108,7 @@ public void DiscoverEndpoints(WolverineHttpOptions wolverineHttpOptions)
_chains.AddRange(calls.Select(x => new HttpChain(x, this)));

wolverineHttpOptions.Middleware.Apply(_chains, Rules, Container);
_optionsWriterPolicies.AddRange(wolverineHttpOptions.ResourceWriterPolicies);

var policies = _options.Policies.OfType<IChainPolicy>();
foreach (var policy in policies) policy.Apply(_chains, Rules, Container);
Expand Down Expand Up @@ -136,7 +138,7 @@ public HttpChain Add(MethodCall method, HttpMethod httpMethod, string url)

internal void UseNewtonsoftJson()
{
_writerPolicies.OfType<JsonResourceWriterPolicy>().Single().Usage = JsonUsage.NewtonsoftJson;
_builtInWriterPolicies.OfType<JsonResourceWriterPolicy>().Single().Usage = JsonUsage.NewtonsoftJson;
_strategies.OfType<JsonBodyParameterStrategy>().Single().Usage = JsonUsage.NewtonsoftJson;
}
}
13 changes: 12 additions & 1 deletion src/Http/Wolverine.Http/Resources/IResourceWriterPolicy.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
namespace Wolverine.Http.Resources;

#region sample_IResourceWriterPolicy
/// <summary>
/// Use to apply custom handling to the primary result of an HTTP endpoint handler
/// </summary>
public interface IResourceWriterPolicy
{
/// <summary>
/// Called during bootstrapping to see whether this policy can handle the chain. If yes no further policies are tried.
/// </summary>
/// <param name="chain"> The chain to test against</param>
/// <returns>True if it applies to the chain, false otherwise</returns>
bool TryApply(HttpChain chain);
}
}

#endregion
Loading
Loading