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
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ const config: UserConfig<DefaultTheme.Config> = {
{text: 'Integration with Marten', link: '/guide/http/marten'},
{text: 'Fluent Validation', link: '/guide/http/fluentvalidation'},
{text: 'Problem Details', link: '/guide/http/problemdetails'},
{text: 'Caching', link: '/guide/http/caching'}
]
},
{
Expand Down
10 changes: 5 additions & 5 deletions docs/guide/http/as-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public class AsParametersQuery{
public int? NullableHeader { get; set; }
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/FormEndpoints.cs#L99-L145' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_as_parameters_binding' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Forms/FormEndpoints.cs#L98-L144' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_as_parameters_binding' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

And the corresponding test case for utilizing this:
Expand Down Expand Up @@ -110,7 +110,7 @@ response.IntegerNotUsed.ShouldBe(default);
response.FloatNotUsed.ShouldBe(default);
response.BooleanNotUsed.ShouldBe(default);
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/Wolverine.Http.Tests/asparameters_binding.cs#L17-L54' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_asparameters_test' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/Wolverine.Http.Tests/asparameters_binding.cs#L18-L55' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_asparameters_test' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Wolverine.HTTP is also able to support `[FromServices]`, `[FromBody]`, and `[FromRoute]` bindings as well
Expand Down Expand Up @@ -152,7 +152,7 @@ public static class AsParametersEndpoints2{
}
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/FormEndpoints.cs#L147-L182' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_as_parameter_for_services_and_body' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Forms/FormEndpoints.cs#L146-L181' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_as_parameter_for_services_and_body' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

And lastly, you can use C# records or really just any constructor function as well
Expand All @@ -173,7 +173,7 @@ public static class AsParameterRecordEndpoint
public static AsParameterRecord Post([AsParameters] AsParameterRecord input) => input;
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/FormEndpoints.cs#L184-L198' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_as_parameter_record' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Forms/FormEndpoints.cs#L183-L197' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_as_parameter_record' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


Expand Down Expand Up @@ -208,5 +208,5 @@ public class ValidatedQuery
}
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/FormEndpoints.cs#L201-L228' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_fluent_validation_with_asparameters' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Forms/FormEndpoints.cs#L200-L227' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_fluent_validation_with_asparameters' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
30 changes: 30 additions & 0 deletions docs/guide/http/caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Caching <Badge type="tip" text="5.9" />

For caching HTTP responses, Wolverine can simply work with the [Response Caching Middleware in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/middleware?view=aspnetcore-10.0).

Wolverine.HTTP *will* respect your usage of the `[ResponseCache]` attribute on either the endpoint handler or method to write out both the `vary` and `cache-control` HTTP headers --
with an attribute on the method taking precedence.

Here's an example or two:

<!-- snippet: sample_using_response_cache_attribute -->
<a id='snippet-sample_using_response_cache_attribute'></a>
```cs
// This is all it takes:
[WolverineGet("/cache/one"), ResponseCache(Duration = 3, VaryByHeader = "accept-encoding", NoStore = false)]
public static string GetOne()
{
return "one";
}

[WolverineGet("/cache/two"), ResponseCache(Duration = 10, NoStore = true)]
public static string GetTwo()
{
return "two";
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/CachedEndpoint.cs#L8-L23' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_response_cache_attribute' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Wolverine.HTTP will also modify the OpenAPI metadata to reflect the caching as well as embed the metadata in the ASP.Net Core
`Endpoint` for utilization inside of the response caching middleware.
2 changes: 1 addition & 1 deletion docs/guide/http/fluentvalidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public class ValidatedQuery
}
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/FormEndpoints.cs#L201-L228' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_fluent_validation_with_asparameters' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Forms/FormEndpoints.cs#L200-L227' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_fluent_validation_with_asparameters' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## QueryString Binding <Badge type="tip" text="5.0" />
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/http/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public async Task use_decimal_form_hit()
body.ReadAsText().ShouldBe("Amount is 42.1");
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/Wolverine.Http.Tests/using_form_parameters.cs#L477-L526' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_form_value_usage' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/Wolverine.Http.Tests/using_form_parameters.cs#L478-L527' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_form_value_usage' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


Expand All @@ -82,7 +82,7 @@ You can also use the FromForm attribute on a complex type, Wolverine will then a
[WolverinePost("/api/fromformbigquery")]
public static BigQuery Post([FromForm] BigQuery query) => query;
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/FormEndpoints.cs#L93-L96' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_[fromform]_binding' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Forms/FormEndpoints.cs#L92-L95' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_[fromform]_binding' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Individual properties on the class can be aliased using ``[FromForm(Name = "aliased")]``
2 changes: 1 addition & 1 deletion docs/guide/http/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ public void RequireAuthorizeOnAll()
ConfigureEndpoints(e => e.RequireAuthorization());
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/Wolverine.Http/WolverineHttpOptions.cs#L229-L239' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_requireauthorizeonall' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/Wolverine.Http/WolverineHttpOptions.cs#L230-L240' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_requireauthorizeonall' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
37 changes: 37 additions & 0 deletions src/Http/Wolverine.Http.Tests/response_cache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using JasperFx.Core.Reflection;
using Shouldly;

namespace Wolverine.Http.Tests;

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

[Fact]
public async Task write_no_header_with_no_attribute()
{
var result = await Scenario(x => x.Get.Url("/cache/none"));
result.Context.Response.Headers.ContainsKey("vary").ShouldBeFalse();
result.Context.Response.Headers.ContainsKey("cache-control").ShouldBeFalse();
}

[Fact]
public async Task write_cache_control_and_vary_by()
{
var result = await Scenario(x => x.Get.Url("/cache/one"));

result.Context.Response.Headers["vary"].Single().ShouldBe("accept-encoding");
result.Context.Response.Headers["cache-control"].Single().ShouldBe("max-age=3");
}

[Fact]
public async Task write_cache_control_no_vary()
{
var result = await Scenario(x => x.Get.Url("/cache/two"));

result.Context.Response.Headers["vary"].Any().ShouldBeFalse();
result.Context.Response.Headers["cache-control"].Single().ShouldBe("no-store, max-age=10");
}
}
51 changes: 51 additions & 0 deletions src/Http/Wolverine.Http/CodeGen/ResponseCacheFrame.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using JasperFx;
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;
using JasperFx.CodeGeneration.Model;
using JasperFx.Core;
using JasperFx.Core.Reflection;
using Microsoft.AspNetCore.Mvc;

namespace Wolverine.Http.CodeGen;

internal class HttpChainResponseCacheHeaderPolicy : IHttpPolicy
{
public void Apply(IReadOnlyList<HttpChain> chains, GenerationRules rules, IServiceContainer container)
{
foreach (var chain in chains.Where(x => x.HasAttribute<ResponseCacheAttribute>()))
{
Apply(chain, container);
}
}

public void Apply(HttpChain chain, IServiceContainer container)
{
if (chain.Method.Method.TryGetAttribute<ResponseCacheAttribute>(out var cache) || chain.Method.HandlerType.TryGetAttribute<ResponseCacheAttribute>(out cache) )
{
chain.Postprocessors.Add(new ResponseCacheFrame(cache));
}
}
}

internal class ResponseCacheFrame : SyncFrame
{
private readonly ResponseCacheAttribute _cache;

public ResponseCacheFrame(ResponseCacheAttribute cache)
{
_cache = cache;
}

public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
{
writer.WriteComment("Write caching headers from a [ResponseCache] usage");
writer.Write($"{nameof(HttpHandler.WriteCacheControls)}(httpContext, {_cache.Duration}, {_cache.NoStore.ToString().ToLowerInvariant()});");

if (_cache.VaryByHeader.IsNotEmpty())
{
writer.Write($"httpContext.Response.Headers[\"vary\"] = {Constant.ForString(_cache.VaryByHeader).Usage};");
}

Next?.GenerateCode(method, writer);
}
}
13 changes: 12 additions & 1 deletion src/Http/Wolverine.Http/HttpHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using Wolverine.Http.Runtime.MultiTenancy;
using JsonSerializer = System.Text.Json.JsonSerializer;

Expand All @@ -25,6 +26,16 @@ public HttpHandler(WolverineHttpOptions wolverineHttpOptions)
_jsonOptions = wolverineHttpOptions.JsonSerializerOptions.Value;
}

public ResponseCacheAttribute? Caching { get; set; }

public void WriteCacheControls(HttpContext context, int maxAgeInSeconds, bool noStore)
{
context.Response.GetTypedHeaders().CacheControl = new()
{
MaxAge = maxAgeInSeconds.Seconds(), NoStore = noStore,
};
}

public async ValueTask<string?> TryDetectTenantId(HttpContext httpContext)
{
var tenantId = await _options.TryDetectTenantId(httpContext);
Expand Down Expand Up @@ -95,7 +106,7 @@ public void ApplyHttpAware(object target, HttpContext context)
{
if (target is IHttpAware a) a.Apply(context);
}

private static bool isRequestJson(HttpContext context)
{
var contentType = context.Request.ContentType;
Expand Down
1 change: 1 addition & 0 deletions src/Http/Wolverine.Http/WolverineHttpOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ public WolverineHttpOptions()
Policies.Add(new HttpAwarePolicy());
Policies.Add(new RequestIdPolicy());
Policies.Add(new RequiredEntityPolicy());
Policies.Add(new HttpChainResponseCacheHeaderPolicy());

Policies.Add(TenantIdDetection);
}
Expand Down
30 changes: 30 additions & 0 deletions src/Http/WolverineWebApi/CachedEndpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Mvc;
using Wolverine.Http;

namespace WolverineWebApi;

public static class CachedEndpoint
{
#region sample_using_response_cache_attribute

// This is all it takes:
[WolverineGet("/cache/one"), ResponseCache(Duration = 3, VaryByHeader = "accept-encoding", NoStore = false)]
public static string GetOne()
{
return "one";
}

[WolverineGet("/cache/two"), ResponseCache(Duration = 10, NoStore = true)]
public static string GetTwo()
{
return "two";
}

#endregion

[WolverineGet("/cache/none")]
public static string GetNone()
{
return "none";
}
}
Loading