Skip to content

Commit 910dc0c

Browse files
committed
Little OpenAPI generation fixes for NSwag usage. Closes GH-685
1 parent ffd304b commit 910dc0c

12 files changed

+304
-3
lines changed
+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using Marten;
2+
using Wolverine;
3+
using Wolverine.Http;
4+
5+
namespace NSwagDemonstrator;
6+
7+
8+
9+
10+
public class Todo
11+
{
12+
public int Id { get; set; }
13+
public string? Name { get; set; }
14+
public bool IsComplete { get; set; }
15+
}
16+
17+
public record CreateTodo(string Name);
18+
19+
public record UpdateTodo(int Id, string Name, bool IsComplete);
20+
21+
public record DeleteTodo(int Id);
22+
23+
public record TodoCreated(int Id);
24+
25+
public static class TodoEndpoints
26+
{
27+
[WolverineGet("/todoitems")]
28+
public static Task<IReadOnlyList<Todo>> Get(IQuerySession session)
29+
=> session.Query<Todo>().ToListAsync();
30+
31+
32+
[WolverineGet("/todoitems/complete")]
33+
public static Task<IReadOnlyList<Todo>> GetComplete(IQuerySession session) =>
34+
session.Query<Todo>().Where(x => x.IsComplete).ToListAsync();
35+
36+
// Wolverine can infer the 200/404 status codes for you here
37+
// so there's no code noise just to satisfy OpenAPI tooling
38+
[WolverineGet("/todoitems/{id}")]
39+
public static Task<Todo?> GetTodo(int id, IQuerySession session, CancellationToken cancellation)
40+
=> session.LoadAsync<Todo>(id, cancellation);
41+
42+
43+
[WolverinePost("/todoitems")]
44+
public static async Task<IResult> Create(CreateTodo command, IDocumentSession session, IMessageBus bus)
45+
{
46+
var todo = new Todo { Name = command.Name };
47+
session.Store(todo);
48+
49+
// Going to raise an event within out system to be processed later
50+
await bus.PublishAsync(new TodoCreated(todo.Id));
51+
52+
return Results.Created($"/todoitems/{todo.Id}", todo);
53+
}
54+
55+
[WolverineDelete("/todoitems")]
56+
public static void Delete(DeleteTodo command, IDocumentSession session)
57+
=> session.Delete<Todo>(command.Id);
58+
}
59+
60+
61+
public static class TodoCreatedHandler
62+
{
63+
// Do something in the background, like assign it to someone,
64+
// send out emails or texts, alerts, whatever
65+
public static void Handle(TodoCreated created, ILogger logger)
66+
{
67+
logger.LogInformation("Got a new TodoCreated event for " + created.Id);
68+
}
69+
}
70+
71+
public static class UpdateTodoEndpoint
72+
{
73+
public static async Task<(Todo? todo, IResult result)> LoadAsync(UpdateTodo command, IDocumentSession session)
74+
{
75+
var todo = await session.LoadAsync<Todo>(command.Id);
76+
return todo != null
77+
? (todo, new WolverineContinue())
78+
: (todo, Results.NotFound());
79+
}
80+
81+
[WolverinePut("/todoitems")]
82+
public static void Put(UpdateTodo command, Todo todo, IDocumentSession session)
83+
{
84+
todo.Name = todo.Name;
85+
todo.IsComplete = todo.IsComplete;
86+
session.Store(todo);
87+
}
88+
}
89+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Wolverine.Http;
2+
3+
namespace NSwagDemonstrator;
4+
5+
#region sample_hello_world_with_wolverine_http
6+
7+
public class HelloEndpoint
8+
{
9+
[WolverineGet("/")]
10+
public string Get() => "Hello.";
11+
}
12+
13+
#endregion
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<InvariantGlobalization>true</InvariantGlobalization>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<ProjectReference Include="..\Wolverine.Http.Marten\Wolverine.Http.Marten.csproj" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<Compile Include="..\..\Servers.cs">
16+
<Link>Servers.cs</Link>
17+
</Compile>
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<!--
22+
To use OpenApiGenerateDocumentsOnBuild with Wolverine, upgrade Microsoft.Extensions.ApiDescription.Server (transitive NSwag dependency).
23+
See also: https://github.com/RicoSuter/NSwag/issues/3403
24+
-->
25+
<!-- <PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="8.0.1"> -->
26+
<!-- <PrivateAssets>all</PrivateAssets> -->
27+
<!-- <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> -->
28+
<!-- </PackageReference> -->
29+
<PackageReference Include="NSwag.AspNetCore" Version="14.0.0" />
30+
</ItemGroup>
31+
32+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@NSwagDemonstrator_HostAddress = http://localhost:5283
2+
3+
GET {{NSwagDemonstrator_HostAddress}}/weatherforecast/
4+
Accept: application/json
5+
6+
###

src/Http/NSwagDemonstrator/Program.cs

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
2+
using IntegrationTests;
3+
using Marten;
4+
using Oakton;
5+
using Oakton.Resources;
6+
using Wolverine;
7+
using Wolverine.Http;
8+
using Wolverine.Marten;
9+
10+
var builder = WebApplication.CreateBuilder(args);
11+
12+
// Adding Marten for persistence
13+
builder.Services.AddMarten(opts =>
14+
{
15+
opts.Connection(Servers.PostgresConnectionString);
16+
opts.DatabaseSchemaName = "todo";
17+
})
18+
.IntegrateWithWolverine();
19+
20+
builder.Services.AddResourceSetupOnStartup();
21+
22+
// Wolverine usage is required for WolverineFx.Http
23+
builder.Host.UseWolverine(opts =>
24+
{
25+
opts.Durability.Mode = DurabilityMode.Solo;
26+
opts.Durability.DurabilityAgentEnabled = false;
27+
28+
// This middleware will apply to the HTTP
29+
// endpoints as well
30+
opts.Policies.AutoApplyTransactions();
31+
32+
// Setting up the outbox on all locally handled
33+
// background tasks
34+
opts.Policies.UseDurableLocalQueues();
35+
});
36+
37+
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
38+
builder.Services.AddEndpointsApiExplorer();
39+
builder.Services.AddOpenApiDocument();
40+
//builder.Services.AddSwaggerDocument();
41+
42+
var app = builder.Build();
43+
44+
app.UseOpenApi(); // serve documents (same as app.UseSwagger())
45+
app.UseSwaggerUi();
46+
//app.UseReDoc(); // serve ReDoc UI
47+
48+
49+
// Let's add in Wolverine HTTP endpoints to the routing tree
50+
app.MapWolverineEndpoints();
51+
52+
// TODO Investigate if this is a dotnet-getdocument issue
53+
args = args.Where(arg => !arg.StartsWith("--applicationName")).ToArray();
54+
55+
return await app.RunOaktonCommands(args);
56+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"$schema": "http://json.schemastore.org/launchsettings.json",
3+
"iisSettings": {
4+
"windowsAuthentication": false,
5+
"anonymousAuthentication": true,
6+
"iisExpress": {
7+
"applicationUrl": "http://localhost:16225",
8+
"sslPort": 44302
9+
}
10+
},
11+
"profiles": {
12+
"http": {
13+
"commandName": "Project",
14+
"dotnetRunMessages": true,
15+
"launchBrowser": true,
16+
"launchUrl": "swagger",
17+
"applicationUrl": "http://localhost:5283",
18+
"environmentVariables": {
19+
"ASPNETCORE_ENVIRONMENT": "Development"
20+
}
21+
},
22+
"https": {
23+
"commandName": "Project",
24+
"dotnetRunMessages": true,
25+
"launchBrowser": true,
26+
"launchUrl": "swagger",
27+
"applicationUrl": "https://localhost:7250;http://localhost:5283",
28+
"environmentVariables": {
29+
"ASPNETCORE_ENVIRONMENT": "Development"
30+
}
31+
},
32+
"IIS Express": {
33+
"commandName": "IISExpress",
34+
"launchBrowser": true,
35+
"launchUrl": "swagger",
36+
"environmentVariables": {
37+
"ASPNETCORE_ENVIRONMENT": "Development"
38+
}
39+
}
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using JasperFx.Core;
2+
using Wolverine.Http;
3+
using Wolverine.Marten;
4+
5+
namespace NSwagDemonstrator;
6+
7+
public record CreateTodoListRequest(string Title);
8+
9+
public class TodoList
10+
{
11+
12+
}
13+
14+
public record TodoListCreated(Guid ListId, string Title);
15+
16+
public static class TodoListEndpoint
17+
{
18+
[WolverinePost("/api/todo-lists")]
19+
public static (IResult, IStartStream) CreateTodoList(
20+
CreateTodoListRequest request,
21+
HttpRequest req
22+
)
23+
{
24+
var listId = CombGuidIdGeneration.NewGuid();
25+
var result = new TodoListCreated(listId, request.Title);
26+
var startStream = MartenOps.StartStream<TodoList>(result);
27+
var response = Results.Created("api/todo-lists/" + listId, result);
28+
29+
return (response, startStream);
30+
}
31+
}
32+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
},
8+
"AllowedHosts": "*"
9+
}

src/Http/Wolverine.Http/HttpChain.ApiDescription.cs

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using System.Collections.Immutable;
2+
using System.Reflection;
23
using JasperFx.Core.Reflection;
34
using Microsoft.AspNetCore.Http;
45
using Microsoft.AspNetCore.Http.Metadata;
56
using Microsoft.AspNetCore.Mvc.Abstractions;
67
using Microsoft.AspNetCore.Mvc.ApiExplorer;
8+
using Microsoft.AspNetCore.Mvc.Controllers;
79
using Microsoft.AspNetCore.Mvc.ModelBinding;
810
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
911
using Microsoft.AspNetCore.Routing;
@@ -14,7 +16,7 @@ namespace Wolverine.Http;
1416
/// <summary>
1517
/// Describes a Wolverine HTTP endpoint implementation
1618
/// </summary>
17-
public class WolverineActionDescriptor : ActionDescriptor
19+
public class WolverineActionDescriptor : ControllerActionDescriptor
1820
{
1921
public WolverineActionDescriptor(HttpChain chain)
2022
{
@@ -23,10 +25,16 @@ public WolverineActionDescriptor(HttpChain chain)
2325
RouteValues["action"] = chain.Method.Method.Name;
2426
Chain = chain;
2527

28+
ControllerTypeInfo = chain.Method.HandlerType.GetTypeInfo();
29+
2630
if (chain.Endpoint != null)
2731
{
2832
EndpointMetadata = chain.Endpoint!.Metadata.ToArray();
2933
}
34+
35+
ActionName = chain.OperationId;
36+
37+
MethodInfo = chain.Method.Method;
3038
}
3139

3240
public override string? DisplayName
@@ -53,7 +61,7 @@ public ApiDescription CreateApiDescription(string httpMethod)
5361
RelativePath = Endpoint.RoutePattern.RawText?.TrimStart('/'),
5462
ActionDescriptor = new WolverineActionDescriptor(this)
5563
};
56-
64+
5765
foreach (var routeParameter in RoutePattern.Parameters)
5866
{
5967
var parameter = buildParameterDescription(routeParameter);

src/Http/Wolverine.Http/WolverineApiDescriptionProvider.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public void OnProvidersExecuting(ApiDescriptionProviderContext context)
3131
{
3232
continue;
3333
}
34-
34+
3535
foreach (var httpMethod in chain.HttpMethods)
3636
{
3737
context.Results.Add(chain.CreateApiDescription(httpMethod));

wolverine.sln

+7
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolverine.Http.Marten", "sr
236236
EndProject
237237
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiDemonstrator", "src\Http\OpenApiDemonstrator\OpenApiDemonstrator.csproj", "{4FA38CED-74C9-4969-83B2-6BD54F245E6C}"
238238
EndProject
239+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NSwagDemonstrator", "src\Http\NSwagDemonstrator\NSwagDemonstrator.csproj", "{DC18FDD3-2AD9-43C9-B8CC-850457FE1A07}"
240+
EndProject
239241
Global
240242
GlobalSection(SolutionConfigurationPlatforms) = preSolution
241243
Debug|Any CPU = Debug|Any CPU
@@ -593,6 +595,10 @@ Global
593595
{4FA38CED-74C9-4969-83B2-6BD54F245E6C}.Debug|Any CPU.Build.0 = Debug|Any CPU
594596
{4FA38CED-74C9-4969-83B2-6BD54F245E6C}.Release|Any CPU.ActiveCfg = Release|Any CPU
595597
{4FA38CED-74C9-4969-83B2-6BD54F245E6C}.Release|Any CPU.Build.0 = Release|Any CPU
598+
{DC18FDD3-2AD9-43C9-B8CC-850457FE1A07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
599+
{DC18FDD3-2AD9-43C9-B8CC-850457FE1A07}.Debug|Any CPU.Build.0 = Debug|Any CPU
600+
{DC18FDD3-2AD9-43C9-B8CC-850457FE1A07}.Release|Any CPU.ActiveCfg = Release|Any CPU
601+
{DC18FDD3-2AD9-43C9-B8CC-850457FE1A07}.Release|Any CPU.Build.0 = Release|Any CPU
596602
EndGlobalSection
597603
GlobalSection(NestedProjects) = preSolution
598604
{24497E6A-D6B1-4C80-ABFB-57FFAD5070C4} = {96119B5E-B5F0-400A-9580-B342EBE26212}
@@ -698,5 +704,6 @@ Global
698704
{6F6FB8FC-564C-4B04-B254-EB53A7E4562F} = {D953D733-D154-4DF2-B2B9-30BF942E1B6B}
699705
{A484AD9E-04C7-4CF9-BB59-5C7DE772851C} = {4B0BC1E5-17F9-4DD0-AC93-DDC522E1BE3C}
700706
{4FA38CED-74C9-4969-83B2-6BD54F245E6C} = {4B0BC1E5-17F9-4DD0-AC93-DDC522E1BE3C}
707+
{DC18FDD3-2AD9-43C9-B8CC-850457FE1A07} = {4B0BC1E5-17F9-4DD0-AC93-DDC522E1BE3C}
701708
EndGlobalSection
702709
EndGlobal

0 commit comments

Comments
 (0)