diff --git a/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Sandbox.csproj b/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Sandbox.csproj index fa3e2095d4e1..f9ba4582232e 100644 --- a/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Sandbox.csproj +++ b/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Sandbox.csproj @@ -1,6 +1,6 @@ - + - $(DefaultNetCoreTargetFramework) + $(DefaultNetCoreTargetFramework) @@ -8,7 +8,10 @@ - - + + + + + \ No newline at end of file diff --git a/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Services/Metadata/PathItemTypeAttribute.cs b/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Services/Metadata/PathItemTypeAttribute.cs new file mode 100644 index 000000000000..d733b64c0e9f --- /dev/null +++ b/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Services/Metadata/PathItemTypeAttribute.cs @@ -0,0 +1,15 @@ +namespace Sandbox.Services.Metadata +{ + public enum PathItemType + { + Standard, + Webhook + } + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate)] + public class PathItemTypeAttribute : Attribute + { + public PathItemType Type { get; } + public PathItemTypeAttribute(PathItemType type) => Type = type; + } +} diff --git a/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Services/PatientEndpointService.cs b/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Services/PatientEndpointService.cs new file mode 100644 index 000000000000..2b208f1eeee4 --- /dev/null +++ b/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Services/PatientEndpointService.cs @@ -0,0 +1,172 @@ +using static Server.Startup; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Writers; +using Sandbox.Services.Metadata; + +namespace Sandbox.Services +{ + public static class PatientEndpointService + { + public static void MapPatientEndpoints(this IEndpointRouteBuilder app) + { + + var patients = new List + { + new Patient(1, "John Doe", "New York", 30), + new Patient(2, "Jane Smith", "Los Angeles", 25), + new Patient(3, "Sam Brown", "Chicago", 40) + }; + //Standard API endpoint + + app.MapGet("/patients", () => patients) + .WithMetadata(new PathItemTypeAttribute(PathItemType.Standard)); + + app.MapGet("/patients/{id}", (int id) => + { + var patient = patients.FirstOrDefault(p => p.id == id); + return patient is not null ? Results.Ok(patient) : Results.NotFound(); + }).WithMetadata(new PathItemTypeAttribute(PathItemType.Standard)); + + app.MapPost("/patients", (Patient patient) => + { + patients.Add(patient); + return Results.Created($"/patients/{patient.id}", patient); + }).WithMetadata(new PathItemTypeAttribute(PathItemType.Standard)); + + //Webhook endpoint + + app.MapPost("/webhook/patientcreated", (Patient patient) => + { + // Process the webhook payload + Console.WriteLine($"Webhook received for patient: {patient.Name}"); + return Results.Ok(); + }).WithMetadata(new PathItemTypeAttribute(PathItemType.Webhook)); + + app.MapGet("/appDocument.json", (IEnumerable source) => + { + var json = CustomOpenApiDocumentGenerator.Generate(source); + return Results.Text(json, "application/json"); + }); + + } + + public record Patient(int id, string Name, string Location, int age); + + } + + static class CustomOpenApiDocumentGenerator + { + public static string Generate(IEnumerable endpointSources) + { + // 1. Build the OpenApiDocument + var document = new OpenApiDocument + { + Info = new OpenApiInfo + { + Title = "MinimalAPI | v1", + Version = "1.0.0" + }, + Paths = new OpenApiPaths() + }; + + // 2. Collect endpoints into groups (Standard/Webhook) and populate Paths + Extensions + var documentGroups = new Dictionary + { + [PathItemType.Standard] = new OpenApiPaths(), + [PathItemType.Webhook] = new OpenApiPaths() + }; + + foreach (var source in endpointSources) + { + foreach (var endpoint in source.Endpoints) + { + var routePattern = (endpoint as RouteEndpoint)?.RoutePattern?.RawText; + if (string.IsNullOrWhiteSpace(routePattern)) continue; + if (routePattern.Equals("/appDocument.json", StringComparison.OrdinalIgnoreCase)) + continue; + + var method = endpoint.Metadata.OfType() + .FirstOrDefault()?.HttpMethods.FirstOrDefault()?.ToUpper() ?? "GET"; + + var type = endpoint.Metadata.OfType().FirstOrDefault()?.Type + ?? PathItemType.Standard; + + var paths = documentGroups[(PathItemType)type!]; + + if (!paths.TryGetValue(routePattern, out var pathItem)) + { + pathItem = new OpenApiPathItem(); + paths[routePattern] = pathItem; + } + + var opType = OperationTypeFromString(method); + + if (!pathItem.Operations.ContainsKey(opType)) + { + pathItem.Operations[opType] = new OpenApiOperation + { + Summary = endpoint.DisplayName, + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse { Description = "OK" } + } + }; + } + } + } + + // Merge Standard paths into document.Paths + foreach (var kv in documentGroups[PathItemType.Standard]) + document.Paths[kv.Key] = kv.Value; + + // Add Webhooks as an extension + if (documentGroups[PathItemType.Webhook].Any()) + { + var webhookObj = new OpenApiObject(); + foreach (var kv in documentGroups[PathItemType.Webhook]) + { + var pathObj = new OpenApiObject(); + foreach (var op in kv.Value.Operations) + { + var opObj = new OpenApiObject + { + ["summary"] = new OpenApiString(op.Value.Summary ?? ""), + ["responses"] = new OpenApiObject + { + ["200"] = new OpenApiObject + { + ["description"] = new OpenApiString("OK") + } + } + }; + pathObj[op.Key.ToString().ToLower()] = opObj; + } + webhookObj[kv.Key] = pathObj; + } + document.Extensions["webhooks"] = webhookObj; + } + + // 3. Serialize OpenApiDocument to JSON string + using var stream = new MemoryStream(); + using var writer = new StreamWriter(stream); + var openApiWriter = new OpenApiJsonWriter(writer); + document.SerializeAsV3(openApiWriter); + writer.Flush(); + stream.Position = 0; + + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + private static OperationType OperationTypeFromString(string method) => method.ToUpper() switch + { + "GET" => OperationType.Get, + "POST" => OperationType.Post, + "PUT" => OperationType.Put, + "PATCH" => OperationType.Patch, + "DELETE" => OperationType.Delete, + _ => OperationType.Get + }; + } +} diff --git a/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Startup.cs b/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Startup.cs index 4106b2bd9cc4..bb83c777021e 100644 --- a/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Startup.cs +++ b/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Startup.cs @@ -45,6 +45,22 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapGrpcService(); endpoints.MapGrpcService(); + endpoints.MapPatientEndpoints(); }); } + + public enum PathItemType + { + Standard, + Webhook + } + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate)] + public class PathItemTypeAttribute : Attribute + { + public PathItemType Type { get; } + public PathItemTypeAttribute(PathItemType type) => Type = type; + } + + }