From 50adfd8a0ef9bb6efaeb530b9246b645928a9ebc Mon Sep 17 00:00:00 2001 From: Nithya Date: Thu, 16 Oct 2025 22:53:31 -0500 Subject: [PATCH 1/2] Clarify Sandbox endpoint separation: Path vs Webhook --- .../test/testassets/Sandbox/Sandbox.csproj | 11 +- .../Services/PatientEndpointService.cs | 181 ++++++++++++++++++ .../test/testassets/Sandbox/Startup.cs | 16 ++ 3 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 src/Grpc/JsonTranscoding/test/testassets/Sandbox/Services/PatientEndpointService.cs 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/PatientEndpointService.cs b/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Services/PatientEndpointService.cs new file mode 100644 index 000000000000..7864738f2bd8 --- /dev/null +++ b/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Services/PatientEndpointService.cs @@ -0,0 +1,181 @@ +using static Server.Startup; + +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); + + public enum PathItemType + { + Standard, + Webhook + } + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate)] + public class PathItemTypeAttribute : Attribute + { + public PathItemType Type { get; } + public PathItemTypeAttribute(PathItemType type) => Type = type; + } + } + + 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[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; + } + + } From 4c35dbd29e9ac285d25c3807b95002913aa903d8 Mon Sep 17 00:00:00 2001 From: Nithya Date: Fri, 17 Oct 2025 10:04:52 -0500 Subject: [PATCH 2/2] Refactor PathItemTypeAttribute for shared metadata and correct webhook classification --- .../Metadata/PathItemTypeAttribute.cs | 15 ++++++++++++ .../Services/PatientEndpointService.cs | 23 ++++++------------- 2 files changed, 22 insertions(+), 16 deletions(-) create mode 100644 src/Grpc/JsonTranscoding/test/testassets/Sandbox/Services/Metadata/PathItemTypeAttribute.cs 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 index 7864738f2bd8..2b208f1eeee4 100644 --- a/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Services/PatientEndpointService.cs +++ b/src/Grpc/JsonTranscoding/test/testassets/Sandbox/Services/PatientEndpointService.cs @@ -1,4 +1,8 @@ using static Server.Startup; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Writers; +using Sandbox.Services.Metadata; namespace Sandbox.Services { @@ -28,8 +32,7 @@ public static void MapPatientEndpoints(this IEndpointRouteBuilder app) { patients.Add(patient); return Results.Created($"/patients/{patient.id}", patient); - }) - .WithMetadata(new PathItemTypeAttribute(PathItemType.Standard)); + }).WithMetadata(new PathItemTypeAttribute(PathItemType.Standard)); //Webhook endpoint @@ -49,19 +52,7 @@ public static void MapPatientEndpoints(this IEndpointRouteBuilder app) } public record Patient(int id, string Name, string Location, int age); - - public enum PathItemType - { - Standard, - Webhook - } - - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate)] - public class PathItemTypeAttribute : Attribute - { - public PathItemType Type { get; } - public PathItemTypeAttribute(PathItemType type) => Type = type; - } + } static class CustomOpenApiDocumentGenerator @@ -101,7 +92,7 @@ public static string Generate(IEnumerable endpointSources) var type = endpoint.Metadata.OfType().FirstOrDefault()?.Type ?? PathItemType.Standard; - var paths = documentGroups[type]; + var paths = documentGroups[(PathItemType)type!]; if (!paths.TryGetValue(routePattern, out var pathItem)) {