Skip to content

Commit

Permalink
Extra docs and a helper method to configure JSON serialization for Wo…
Browse files Browse the repository at this point in the history
…lverine.HTTP. Closes GH-552
  • Loading branch information
jeremydmiller committed Sep 22, 2023
1 parent 1b33534 commit 2789d48
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 83 deletions.
1 change: 1 addition & 0 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export default {
{text: 'Http Services with Wolverine', link: '/guide/http/'},
{text: 'Integration with ASP.Net Core', link: '/guide/http/integration'},
{text: 'Endpoints', link: '/guide/http/endpoints'},
{text: 'Json', link: '/guide/http/json'},
{text: 'Routing', link: '/guide/http/routing'},
{text: 'Authentication and Authorization', link: '/guide/http/security'},
{text: 'Working with Querystring', link: '/guide/http/querystring'},
Expand Down
80 changes: 2 additions & 78 deletions docs/guide/http/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,83 +171,7 @@ public static OrderShipped Ship(ShipOrder command, Order order)

## JSON Handling

::: warning
At this point WolverineFx.Http **only** supports `System.Text.Json` as the default for the HTTP endpoints,
with the JSON settings coming from the application's Minimal API configuration.
:::

As explained up above, the "request" type to a Wolverine endpoint is the first argument that is:

1. Concrete
2. Not one of the value types that Wolverine considers for route or query string values
3. *Not* marked with `[FromServices]` from ASP.Net Core

If a parameter like this exists, that will be the request type, and will come
at runtime from deserializing the HTTP request body as JSON.

Likewise, any resource type besides strings will be written to the HTTP response body
as serialized JSON.

In this sample endpoint, both the request and resource types are dealt with by
JSON serialization. Here's the test from the actual Wolverine codebase:

<!-- snippet: sample_post_json_happy_path -->
<a id='snippet-sample_post_json_happy_path'></a>
```cs
[Fact]
public async Task post_json_happy_path()
{
// This test is using Alba to run an end to end HTTP request
// and interrogate the results
var response = await Scenario(x =>
{
x.Post.Json(new Question { One = 3, Two = 4 }).ToUrl("/question");
x.WithRequestHeader("accept", "application/json");
});

var result = await response.ReadAsJsonAsync<ArithmeticResults>();

result.Product.ShouldBe(12);
result.Sum.ShouldBe(7);
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/Wolverine.Http.Tests/posting_json.cs#L12-L31' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_post_json_happy_path' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

### Using Newtonsoft.Json

::: TIP
Newtonsoft.Json is still much more battle hardened than System.Text.Json, and you may need
to drop back to Newtonsoft.Json for various scenarios. This feature was added specifically
at the request of F# developers.
:::

To opt into using Newtonsoft.Json for the JSON serialization of *HTTP endpoints*, you have this option within the call
to the `MapWolverineEndpoints()` configuration:

<!-- snippet: sample_use_newtonsoft_for_http_serialization -->
<a id='snippet-sample_use_newtonsoft_for_http_serialization'></a>
```cs
var builder = WebApplication.CreateBuilder(Array.Empty<string>());
builder.Services.AddScoped<IUserService, UserService>();

builder.Services.AddMarten(Servers.PostgresConnectionString)
.IntegrateWithWolverine();

builder.Host.UseWolverine();

await using var host = await AlbaHost.For(builder, app =>
{
app.MapWolverineEndpoints(opts =>
{
// Opt into using Newtonsoft.Json for JSON serialization just with Wolverine.HTTP routes
// Configuring the JSON serialization is optional
opts.UseNewtonsoftJsonForSerialization(settings => settings.TypeNameHandling = TypeNameHandling.All);
});
});
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/Wolverine.Http.Tests/using_newtonsoft_for_serialization.cs#L18-L38' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_use_newtonsoft_for_http_serialization' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
See [JSON serialization for more information](/json)

## Returning Strings

Expand Down Expand Up @@ -387,7 +311,7 @@ and register that strategy within our `MapWolverineEndpoints()` set up like so:
// Customizing parameter handling
opts.AddParameterHandlingStrategy<NowParameterStrategy>();
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L128-L133' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_adding_custom_parameter_handling' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L130-L135' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_adding_custom_parameter_handling' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

And lastly, here's the application within an HTTP endpoint for extra context:
Expand Down
116 changes: 116 additions & 0 deletions docs/guide/http/json.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# JSON Serialization

::: warning
At this point WolverineFx.Http **only** supports `System.Text.Json` as the default for the HTTP endpoints,
with the JSON settings coming from the application's Minimal API configuration.
:::

::: tip
You can tell Wolverine to ignore all return values as the request body by decorating either the endpoint
method or the whole endpoint class with `[EmptyResponse]`
:::

As explained up above, the "request" type to a Wolverine endpoint is the first argument that is:

1. Concrete
2. Not one of the value types that Wolverine considers for route or query string values
3. *Not* marked with `[FromServices]` from ASP.Net Core

If a parameter like this exists, that will be the request type, and will come
at runtime from deserializing the HTTP request body as JSON.

Likewise, any resource type besides strings will be written to the HTTP response body
as serialized JSON.

In this sample endpoint, both the request and resource types are dealt with by
JSON serialization. Here's the test from the actual Wolverine codebase:

<!-- snippet: sample_post_json_happy_path -->
<a id='snippet-sample_post_json_happy_path'></a>
```cs
[Fact]
public async Task post_json_happy_path()
{
// This test is using Alba to run an end to end HTTP request
// and interrogate the results
var response = await Scenario(x =>
{
x.Post.Json(new Question { One = 3, Two = 4 }).ToUrl("/question");
x.WithRequestHeader("accept", "application/json");
});

var result = await response.ReadAsJsonAsync<ArithmeticResults>();

result.Product.ShouldBe(12);
result.Sum.ShouldBe(7);
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/Wolverine.Http.Tests/posting_json.cs#L12-L31' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_post_json_happy_path' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Configuring System.Text.Json

Wolverine depends on the value of the `IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>` value registered in your application container for System.Text.Json
configuration.

But, because there are multiple `JsonOption` types in the AspNetCore world and it's way too easy to pick the wrong one
and get confused and angry about why your configuration isn't impacting Wolverine, there's this extension method helper
that will do the right thing behind the scenes:

<!-- snippet: sample_configuring_stj_for_wolverine -->
<a id='snippet-sample_configuring_stj_for_wolverine'></a>
```cs
var builder = WebApplication.CreateBuilder();

builder.Host.UseWolverine();

builder.Services.ConfigureSystemTextJsonForWolverineOrMinimalApi(o =>
{
// Do whatever you want here to customize the JSON
// serialization
o.SerializerOptions.WriteIndented = true;
});

var app = builder.Build();

app.MapWolverineEndpoints();

return await app.RunOaktonCommands(args);
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/Wolverine.Http.Tests/Samples/ConfiguringJson.cs#L10-L29' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_configuring_stj_for_wolverine' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Using Newtonsoft.Json

::: tip
Newtonsoft.Json is still much more battle hardened than System.Text.Json, and you may need
to drop back to Newtonsoft.Json for various scenarios. This feature was added specifically
at the request of F# developers.
:::

To opt into using Newtonsoft.Json for the JSON serialization of *HTTP endpoints*, you have this option within the call
to the `MapWolverineEndpoints()` configuration:

<!-- snippet: sample_use_newtonsoft_for_http_serialization -->
<a id='snippet-sample_use_newtonsoft_for_http_serialization'></a>
```cs
var builder = WebApplication.CreateBuilder(Array.Empty<string>());
builder.Services.AddScoped<IUserService, UserService>();

builder.Services.AddMarten(Servers.PostgresConnectionString)
.IntegrateWithWolverine();

builder.Host.UseWolverine();

await using var host = await AlbaHost.For(builder, app =>
{
app.MapWolverineEndpoints(opts =>
{
// Opt into using Newtonsoft.Json for JSON serialization just with Wolverine.HTTP routes
// Configuring the JSON serialization is optional
opts.UseNewtonsoftJsonForSerialization(settings => settings.TypeNameHandling = TypeNameHandling.All);
});
});
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/Wolverine.Http.Tests/using_newtonsoft_for_serialization.cs#L18-L38' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_use_newtonsoft_for_http_serialization' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
2 changes: 1 addition & 1 deletion docs/guide/http/mediator.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ app.MapPostToWolverine<CustomRequest, CustomResponse>("/wolverine/request");
app.MapDeleteToWolverine<CustomRequest, CustomResponse>("/wolverine/request");
app.MapPutToWolverine<CustomRequest, CustomResponse>("/wolverine/request");
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L137-L149' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_optimized_mediator_usage' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L139-L151' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_optimized_mediator_usage' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

With this mechanism, Wolverine is able to optimize the runtime function for Minimal API by eliminating IoC service locations
Expand Down
6 changes: 3 additions & 3 deletions docs/guide/http/messaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public static async ValueTask SendViaMessageBus(IMessageBus bus)
await bus.PublishAsync(new HttpMessage2("bar"));
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/MessageHandlers.cs#L31-L41' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_publishing_cascading_messages_from_http_endpoint_with_imessagebus' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/MessageHandlers.cs#L33-L43' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_publishing_cascading_messages_from_http_endpoint_with_imessagebus' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

But of course there's some other alternatives to directly using `IMessageBus` by utilizing Wolverine's [cascading messages](/guide/handlers/cascading)
Expand Down Expand Up @@ -121,7 +121,7 @@ public static (string, OutgoingMessages) Post(SpawnInput input)
return ("got it", messages);
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/MessageHandlers.cs#L55-L72' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_spawning_messages_from_http_endpoint_via_outgoingmessages' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/MessageHandlers.cs#L57-L74' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_spawning_messages_from_http_endpoint_via_outgoingmessages' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Otherwise, if you want to make it clearer from the signature of your HTTP handler method what messages are cascaded
Expand All @@ -138,5 +138,5 @@ public static (HttpMessage1, HttpMessage2) Post()
return new(new HttpMessage1("foo"), new HttpMessage2("bar"));
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/MessageHandlers.cs#L43-L53' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_publishing_cascading_messages_from_http_endpoint' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/MessageHandlers.cs#L45-L55' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_publishing_cascading_messages_from_http_endpoint' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->
31 changes: 31 additions & 0 deletions src/Http/Wolverine.Http.Tests/Samples/ConfiguringJson.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Builder;
using Oakton;

namespace Wolverine.Http.Tests.Samples;

public class ConfiguringJson
{
public static async Task<int> configure(params string[] args)
{
#region sample_configuring_stj_for_wolverine

var builder = WebApplication.CreateBuilder();

builder.Host.UseWolverine();

builder.Services.ConfigureSystemTextJsonForWolverineOrMinimalApi(o =>
{
// Do whatever you want here to customize the JSON
// serialization
o.SerializerOptions.WriteIndented = true;
});

var app = builder.Build();

app.MapWolverineEndpoints();

return await app.RunOaktonCommands(args);

#endregion
}
}
9 changes: 9 additions & 0 deletions src/Http/Wolverine.Http/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Http.Json;
using Microsoft.Extensions.DependencyInjection;

namespace Wolverine.Http;

public static class ServiceCollectionExtensions
{

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ public WolverineRequiredException(Exception? innerException) : base(

public static class WolverineHttpEndpointRouteBuilderExtensions
{
/// <summary>
/// Just a helper to configure the correct JsonOptions used by both Wolverine and Minimal API
/// </summary>
/// <param name="services"></param>
/// <param name="configure"></param>
public static void ConfigureSystemTextJsonForWolverineOrMinimalApi(this IServiceCollection services,
Action<JsonOptions> configure)
{
services.Configure<JsonOptions>(configure);
}

/// <summary>
/// Use the request body of type T to immediately invoke the incoming command with Wolverine
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public static Task<IReadOnlyList<Todo>> GetComplete(IQuerySession session)
// Wolverine can infer the 200/404 status codes for you here
// so there's no code noise just to satisfy OpenAPI tooling
[WolverineGet("/todoitems/{tenant}/{id}")]
public static Task<Todo?> GetTodo(int id, string tenant, IQuerySession session, CancellationToken cancellation)
public static Task<Todo?> GetTodo(int id, IQuerySession session, CancellationToken cancellation)
{
return session.LoadAsync<Todo>(id, cancellation);
}
Expand Down

0 comments on commit 2789d48

Please sign in to comment.