From 910dc0c4f291223f0a01fab944ed81917507a8b5 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Tue, 16 Jan 2024 06:34:45 -0600 Subject: [PATCH] Little OpenAPI generation fixes for NSwag usage. Closes GH-685 --- src/Http/NSwagDemonstrator/Endpoints.cs | 89 +++++++++++++++++++ src/Http/NSwagDemonstrator/HelloEndpoint.cs | 13 +++ .../NSwagDemonstrator.csproj | 32 +++++++ .../NSwagDemonstrator/NSwagDemonstrator.http | 6 ++ src/Http/NSwagDemonstrator/Program.cs | 56 ++++++++++++ .../Properties/launchSettings.json | 41 +++++++++ .../NSwagDemonstrator/TodoListEndpoint.cs | 32 +++++++ .../appsettings.Development.json | 8 ++ src/Http/NSwagDemonstrator/appsettings.json | 9 ++ .../HttpChain.ApiDescription.cs | 12 ++- .../WolverineApiDescriptionProvider.cs | 2 +- wolverine.sln | 7 ++ 12 files changed, 304 insertions(+), 3 deletions(-) create mode 100644 src/Http/NSwagDemonstrator/Endpoints.cs create mode 100644 src/Http/NSwagDemonstrator/HelloEndpoint.cs create mode 100644 src/Http/NSwagDemonstrator/NSwagDemonstrator.csproj create mode 100644 src/Http/NSwagDemonstrator/NSwagDemonstrator.http create mode 100644 src/Http/NSwagDemonstrator/Program.cs create mode 100644 src/Http/NSwagDemonstrator/Properties/launchSettings.json create mode 100644 src/Http/NSwagDemonstrator/TodoListEndpoint.cs create mode 100644 src/Http/NSwagDemonstrator/appsettings.Development.json create mode 100644 src/Http/NSwagDemonstrator/appsettings.json diff --git a/src/Http/NSwagDemonstrator/Endpoints.cs b/src/Http/NSwagDemonstrator/Endpoints.cs new file mode 100644 index 000000000..3495e30b1 --- /dev/null +++ b/src/Http/NSwagDemonstrator/Endpoints.cs @@ -0,0 +1,89 @@ +using Marten; +using Wolverine; +using Wolverine.Http; + +namespace NSwagDemonstrator; + + + + +public class Todo +{ + public int Id { get; set; } + public string? Name { get; set; } + public bool IsComplete { get; set; } +} + +public record CreateTodo(string Name); + +public record UpdateTodo(int Id, string Name, bool IsComplete); + +public record DeleteTodo(int Id); + +public record TodoCreated(int Id); + +public static class TodoEndpoints +{ + [WolverineGet("/todoitems")] + public static Task> Get(IQuerySession session) + => session.Query().ToListAsync(); + + + [WolverineGet("/todoitems/complete")] + public static Task> GetComplete(IQuerySession session) => + session.Query().Where(x => x.IsComplete).ToListAsync(); + + // Wolverine can infer the 200/404 status codes for you here + // so there's no code noise just to satisfy OpenAPI tooling + [WolverineGet("/todoitems/{id}")] + public static Task GetTodo(int id, IQuerySession session, CancellationToken cancellation) + => session.LoadAsync(id, cancellation); + + + [WolverinePost("/todoitems")] + public static async Task Create(CreateTodo command, IDocumentSession session, IMessageBus bus) + { + var todo = new Todo { Name = command.Name }; + session.Store(todo); + + // Going to raise an event within out system to be processed later + await bus.PublishAsync(new TodoCreated(todo.Id)); + + return Results.Created($"/todoitems/{todo.Id}", todo); + } + + [WolverineDelete("/todoitems")] + public static void Delete(DeleteTodo command, IDocumentSession session) + => session.Delete(command.Id); +} + + +public static class TodoCreatedHandler +{ + // Do something in the background, like assign it to someone, + // send out emails or texts, alerts, whatever + public static void Handle(TodoCreated created, ILogger logger) + { + logger.LogInformation("Got a new TodoCreated event for " + created.Id); + } +} + +public static class UpdateTodoEndpoint +{ + public static async Task<(Todo? todo, IResult result)> LoadAsync(UpdateTodo command, IDocumentSession session) + { + var todo = await session.LoadAsync(command.Id); + return todo != null + ? (todo, new WolverineContinue()) + : (todo, Results.NotFound()); + } + + [WolverinePut("/todoitems")] + public static void Put(UpdateTodo command, Todo todo, IDocumentSession session) + { + todo.Name = todo.Name; + todo.IsComplete = todo.IsComplete; + session.Store(todo); + } +} + diff --git a/src/Http/NSwagDemonstrator/HelloEndpoint.cs b/src/Http/NSwagDemonstrator/HelloEndpoint.cs new file mode 100644 index 000000000..20f928925 --- /dev/null +++ b/src/Http/NSwagDemonstrator/HelloEndpoint.cs @@ -0,0 +1,13 @@ +using Wolverine.Http; + +namespace NSwagDemonstrator; + +#region sample_hello_world_with_wolverine_http + +public class HelloEndpoint +{ + [WolverineGet("/")] + public string Get() => "Hello."; +} + +#endregion \ No newline at end of file diff --git a/src/Http/NSwagDemonstrator/NSwagDemonstrator.csproj b/src/Http/NSwagDemonstrator/NSwagDemonstrator.csproj new file mode 100644 index 000000000..bceae0e34 --- /dev/null +++ b/src/Http/NSwagDemonstrator/NSwagDemonstrator.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + Servers.cs + + + + + + + + + + + + + diff --git a/src/Http/NSwagDemonstrator/NSwagDemonstrator.http b/src/Http/NSwagDemonstrator/NSwagDemonstrator.http new file mode 100644 index 000000000..7e8b0865b --- /dev/null +++ b/src/Http/NSwagDemonstrator/NSwagDemonstrator.http @@ -0,0 +1,6 @@ +@NSwagDemonstrator_HostAddress = http://localhost:5283 + +GET {{NSwagDemonstrator_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/Http/NSwagDemonstrator/Program.cs b/src/Http/NSwagDemonstrator/Program.cs new file mode 100644 index 000000000..82b49a419 --- /dev/null +++ b/src/Http/NSwagDemonstrator/Program.cs @@ -0,0 +1,56 @@ + +using IntegrationTests; +using Marten; +using Oakton; +using Oakton.Resources; +using Wolverine; +using Wolverine.Http; +using Wolverine.Marten; + +var builder = WebApplication.CreateBuilder(args); + +// Adding Marten for persistence +builder.Services.AddMarten(opts => +{ + opts.Connection(Servers.PostgresConnectionString); + opts.DatabaseSchemaName = "todo"; +}) + .IntegrateWithWolverine(); + +builder.Services.AddResourceSetupOnStartup(); + +// Wolverine usage is required for WolverineFx.Http +builder.Host.UseWolverine(opts => +{ + opts.Durability.Mode = DurabilityMode.Solo; + opts.Durability.DurabilityAgentEnabled = false; + + // This middleware will apply to the HTTP + // endpoints as well + opts.Policies.AutoApplyTransactions(); + + // Setting up the outbox on all locally handled + // background tasks + opts.Policies.UseDurableLocalQueues(); +}); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddOpenApiDocument(); +//builder.Services.AddSwaggerDocument(); + +var app = builder.Build(); + +app.UseOpenApi(); // serve documents (same as app.UseSwagger()) +app.UseSwaggerUi(); +//app.UseReDoc(); // serve ReDoc UI + + +// Let's add in Wolverine HTTP endpoints to the routing tree +app.MapWolverineEndpoints(); + +// TODO Investigate if this is a dotnet-getdocument issue +args = args.Where(arg => !arg.StartsWith("--applicationName")).ToArray(); + +return await app.RunOaktonCommands(args); + diff --git a/src/Http/NSwagDemonstrator/Properties/launchSettings.json b/src/Http/NSwagDemonstrator/Properties/launchSettings.json new file mode 100644 index 000000000..afdb8c59b --- /dev/null +++ b/src/Http/NSwagDemonstrator/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:16225", + "sslPort": 44302 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5283", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7250;http://localhost:5283", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Http/NSwagDemonstrator/TodoListEndpoint.cs b/src/Http/NSwagDemonstrator/TodoListEndpoint.cs new file mode 100644 index 000000000..ba68d7f46 --- /dev/null +++ b/src/Http/NSwagDemonstrator/TodoListEndpoint.cs @@ -0,0 +1,32 @@ +using JasperFx.Core; +using Wolverine.Http; +using Wolverine.Marten; + +namespace NSwagDemonstrator; + +public record CreateTodoListRequest(string Title); + +public class TodoList +{ + +} + +public record TodoListCreated(Guid ListId, string Title); + +public static class TodoListEndpoint +{ + [WolverinePost("/api/todo-lists")] + public static (IResult, IStartStream) CreateTodoList( + CreateTodoListRequest request, + HttpRequest req + ) + { + var listId = CombGuidIdGeneration.NewGuid(); + var result = new TodoListCreated(listId, request.Title); + var startStream = MartenOps.StartStream(result); + var response = Results.Created("api/todo-lists/" + listId, result); + + return (response, startStream); + } +} + diff --git a/src/Http/NSwagDemonstrator/appsettings.Development.json b/src/Http/NSwagDemonstrator/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/src/Http/NSwagDemonstrator/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Http/NSwagDemonstrator/appsettings.json b/src/Http/NSwagDemonstrator/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/src/Http/NSwagDemonstrator/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Http/Wolverine.Http/HttpChain.ApiDescription.cs b/src/Http/Wolverine.Http/HttpChain.ApiDescription.cs index 0a60bae02..4e49d22eb 100644 --- a/src/Http/Wolverine.Http/HttpChain.ApiDescription.cs +++ b/src/Http/Wolverine.Http/HttpChain.ApiDescription.cs @@ -1,9 +1,11 @@ using System.Collections.Immutable; +using System.Reflection; using JasperFx.Core.Reflection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Routing; @@ -14,7 +16,7 @@ namespace Wolverine.Http; /// /// Describes a Wolverine HTTP endpoint implementation /// -public class WolverineActionDescriptor : ActionDescriptor +public class WolverineActionDescriptor : ControllerActionDescriptor { public WolverineActionDescriptor(HttpChain chain) { @@ -23,10 +25,16 @@ public WolverineActionDescriptor(HttpChain chain) RouteValues["action"] = chain.Method.Method.Name; Chain = chain; + ControllerTypeInfo = chain.Method.HandlerType.GetTypeInfo(); + if (chain.Endpoint != null) { EndpointMetadata = chain.Endpoint!.Metadata.ToArray(); } + + ActionName = chain.OperationId; + + MethodInfo = chain.Method.Method; } public override string? DisplayName @@ -53,7 +61,7 @@ public ApiDescription CreateApiDescription(string httpMethod) RelativePath = Endpoint.RoutePattern.RawText?.TrimStart('/'), ActionDescriptor = new WolverineActionDescriptor(this) }; - + foreach (var routeParameter in RoutePattern.Parameters) { var parameter = buildParameterDescription(routeParameter); diff --git a/src/Http/Wolverine.Http/WolverineApiDescriptionProvider.cs b/src/Http/Wolverine.Http/WolverineApiDescriptionProvider.cs index 7800469a9..b2bed1939 100644 --- a/src/Http/Wolverine.Http/WolverineApiDescriptionProvider.cs +++ b/src/Http/Wolverine.Http/WolverineApiDescriptionProvider.cs @@ -31,7 +31,7 @@ public void OnProvidersExecuting(ApiDescriptionProviderContext context) { continue; } - + foreach (var httpMethod in chain.HttpMethods) { context.Results.Add(chain.CreateApiDescription(httpMethod)); diff --git a/wolverine.sln b/wolverine.sln index 1d38cf473..137b103b2 100644 --- a/wolverine.sln +++ b/wolverine.sln @@ -236,6 +236,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolverine.Http.Marten", "sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiDemonstrator", "src\Http\OpenApiDemonstrator\OpenApiDemonstrator.csproj", "{4FA38CED-74C9-4969-83B2-6BD54F245E6C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NSwagDemonstrator", "src\Http\NSwagDemonstrator\NSwagDemonstrator.csproj", "{DC18FDD3-2AD9-43C9-B8CC-850457FE1A07}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -593,6 +595,10 @@ Global {4FA38CED-74C9-4969-83B2-6BD54F245E6C}.Debug|Any CPU.Build.0 = Debug|Any CPU {4FA38CED-74C9-4969-83B2-6BD54F245E6C}.Release|Any CPU.ActiveCfg = Release|Any CPU {4FA38CED-74C9-4969-83B2-6BD54F245E6C}.Release|Any CPU.Build.0 = Release|Any CPU + {DC18FDD3-2AD9-43C9-B8CC-850457FE1A07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC18FDD3-2AD9-43C9-B8CC-850457FE1A07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC18FDD3-2AD9-43C9-B8CC-850457FE1A07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC18FDD3-2AD9-43C9-B8CC-850457FE1A07}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {24497E6A-D6B1-4C80-ABFB-57FFAD5070C4} = {96119B5E-B5F0-400A-9580-B342EBE26212} @@ -698,5 +704,6 @@ Global {6F6FB8FC-564C-4B04-B254-EB53A7E4562F} = {D953D733-D154-4DF2-B2B9-30BF942E1B6B} {A484AD9E-04C7-4CF9-BB59-5C7DE772851C} = {4B0BC1E5-17F9-4DD0-AC93-DDC522E1BE3C} {4FA38CED-74C9-4969-83B2-6BD54F245E6C} = {4B0BC1E5-17F9-4DD0-AC93-DDC522E1BE3C} + {DC18FDD3-2AD9-43C9-B8CC-850457FE1A07} = {4B0BC1E5-17F9-4DD0-AC93-DDC522E1BE3C} EndGlobalSection EndGlobal