From c8cea0a4a94ecf9953c87bf08a7f8a87c3bc0b75 Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Thu, 3 Oct 2024 15:50:29 -0700 Subject: [PATCH 1/2] (#118) Added new NSwag schema processing for base typing --- .../Sample.Datasync.Server.sln | 17 ++++++++ .../src/Sample.Datasync.Server/Program.cs | 27 +++++++++++++ .../Sample.Datasync.Server.csproj | 2 + .../appsettings.Development.json | 3 ++ .../DatasyncOperationProcessor.cs | 40 +++++++++++++++++++ 5 files changed, 89 insertions(+) diff --git a/samples/datasync-server/Sample.Datasync.Server.sln b/samples/datasync-server/Sample.Datasync.Server.sln index bf831c04..bb667052 100644 --- a/samples/datasync-server/Sample.Datasync.Server.sln +++ b/samples/datasync-server/Sample.Datasync.Server.sln @@ -15,6 +15,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Server.EntityFrameworkCore", "..\..\src\CommunityToolkit.Datasync.Server.EntityFrameworkCore\CommunityToolkit.Datasync.Server.EntityFrameworkCore.csproj", "{2086DD5C-C7C1-4957-B667-847C5FEE832C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Server.NSwag", "..\..\src\CommunityToolkit.Datasync.Server.NSwag\CommunityToolkit.Datasync.Server.NSwag.csproj", "{3DD18C86-10C3-490B-A7A6-C2B83C8A3B29}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Server.Swashbuckle", "..\..\src\CommunityToolkit.Datasync.Server.Swashbuckle\CommunityToolkit.Datasync.Server.Swashbuckle.csproj", "{FEE92211-3A57-420D-8A76-77AD23B015B6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -37,6 +41,14 @@ Global {2086DD5C-C7C1-4957-B667-847C5FEE832C}.Debug|Any CPU.Build.0 = Debug|Any CPU {2086DD5C-C7C1-4957-B667-847C5FEE832C}.Release|Any CPU.ActiveCfg = Release|Any CPU {2086DD5C-C7C1-4957-B667-847C5FEE832C}.Release|Any CPU.Build.0 = Release|Any CPU + {3DD18C86-10C3-490B-A7A6-C2B83C8A3B29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3DD18C86-10C3-490B-A7A6-C2B83C8A3B29}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DD18C86-10C3-490B-A7A6-C2B83C8A3B29}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3DD18C86-10C3-490B-A7A6-C2B83C8A3B29}.Release|Any CPU.Build.0 = Release|Any CPU + {FEE92211-3A57-420D-8A76-77AD23B015B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FEE92211-3A57-420D-8A76-77AD23B015B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FEE92211-3A57-420D-8A76-77AD23B015B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FEE92211-3A57-420D-8A76-77AD23B015B6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -46,5 +58,10 @@ Global {54E9A0B2-A0B0-4CB1-8FAD-11DB9E4535A6} = {95358590-6440-469A-8A6A-6ACC47F52966} {DEC37ED1-B52A-4287-8F63-8210328AFF70} = {95358590-6440-469A-8A6A-6ACC47F52966} {2086DD5C-C7C1-4957-B667-847C5FEE832C} = {95358590-6440-469A-8A6A-6ACC47F52966} + {3DD18C86-10C3-490B-A7A6-C2B83C8A3B29} = {95358590-6440-469A-8A6A-6ACC47F52966} + {FEE92211-3A57-420D-8A76-77AD23B015B6} = {95358590-6440-469A-8A6A-6ACC47F52966} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B0373E78-5E78-44A4-A907-798EAC85F597} EndGlobalSection EndGlobal diff --git a/samples/datasync-server/src/Sample.Datasync.Server/Program.cs b/samples/datasync-server/src/Sample.Datasync.Server/Program.cs index 1f0c8c34..a7b302ba 100644 --- a/samples/datasync-server/src/Sample.Datasync.Server/Program.cs +++ b/samples/datasync-server/src/Sample.Datasync.Server/Program.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.Datasync.Server; +using CommunityToolkit.Datasync.Server.NSwag; +using CommunityToolkit.Datasync.Server.Swashbuckle; using Microsoft.EntityFrameworkCore; using Sample.Datasync.Server.Db; @@ -11,10 +13,24 @@ string connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new ApplicationException("DefaultConnection is not set"); +string? swaggerDriver = builder.Configuration["Swagger:Driver"]; +bool nswagEnabled = swaggerDriver?.Equals("NSwag", StringComparison.InvariantCultureIgnoreCase) == true; +bool swashbuckleEnabled = swaggerDriver?.Equals("Swashbuckle", StringComparison.InvariantCultureIgnoreCase) == true; + builder.Services.AddDbContext(options => options.UseSqlServer(connectionString)); builder.Services.AddDatasyncServices(); builder.Services.AddControllers(); +if (nswagEnabled) +{ + _ = builder.Services.AddOpenApiDocument(options => options.AddDatasyncProcessor()); +} + +if (swashbuckleEnabled) +{ + _ = builder.Services.AddSwaggerGen(options => options.AddDatasyncControllers()); +} + WebApplication app = builder.Build(); // Initialize the database @@ -25,6 +41,17 @@ } app.UseHttpsRedirection(); + +if (nswagEnabled) +{ + _ = app.UseOpenApi().UseSwaggerUI(); +} + +if (swashbuckleEnabled) +{ + _ = app.UseSwagger().UseSwaggerUI(); +} + app.UseAuthorization(); app.MapControllers(); diff --git a/samples/datasync-server/src/Sample.Datasync.Server/Sample.Datasync.Server.csproj b/samples/datasync-server/src/Sample.Datasync.Server/Sample.Datasync.Server.csproj index 8287f2b7..d4c6e006 100644 --- a/samples/datasync-server/src/Sample.Datasync.Server/Sample.Datasync.Server.csproj +++ b/samples/datasync-server/src/Sample.Datasync.Server/Sample.Datasync.Server.csproj @@ -24,6 +24,8 @@ + + diff --git a/samples/datasync-server/src/Sample.Datasync.Server/appsettings.Development.json b/samples/datasync-server/src/Sample.Datasync.Server/appsettings.Development.json index 0c208ae9..cc0551f0 100644 --- a/samples/datasync-server/src/Sample.Datasync.Server/appsettings.Development.json +++ b/samples/datasync-server/src/Sample.Datasync.Server/appsettings.Development.json @@ -4,5 +4,8 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "Swagger": { + "Driver": "NSwag" } } diff --git a/src/CommunityToolkit.Datasync.Server.NSwag/DatasyncOperationProcessor.cs b/src/CommunityToolkit.Datasync.Server.NSwag/DatasyncOperationProcessor.cs index 433049f5..e0662b75 100644 --- a/src/CommunityToolkit.Datasync.Server.NSwag/DatasyncOperationProcessor.cs +++ b/src/CommunityToolkit.Datasync.Server.NSwag/DatasyncOperationProcessor.cs @@ -65,6 +65,7 @@ private static void ProcessDatasyncOperation(OperationProcessorContext context) string path = context.OperationDescription.Path; Type entityType = GetTableEntityType(context.ControllerType); JsonSchema entitySchemaRef = GetEntityReference(context, entityType); + AddMissingSchemaProperties(entitySchemaRef.Reference); if (method.Equals("DELETE", StringComparison.InvariantCultureIgnoreCase)) { @@ -91,6 +92,8 @@ private static void ProcessDatasyncOperation(OperationProcessorContext context) if (method.Equals("POST", StringComparison.InvariantCultureIgnoreCase)) { operation.AddConditionalRequestSupport(entitySchemaRef, true); + operation.TryAddConsumes("application/json"); + operation.Parameters.Add(new OpenApiParameter { Schema = entitySchemaRef, Kind = OpenApiParameterKind.Body }); operation.SetResponse(HttpStatusCode.Created, entitySchemaRef); operation.SetResponse(HttpStatusCode.BadRequest); } @@ -98,6 +101,8 @@ private static void ProcessDatasyncOperation(OperationProcessorContext context) if (method.Equals("PUT", StringComparison.InvariantCultureIgnoreCase)) { operation.AddConditionalRequestSupport(entitySchemaRef); + operation.TryAddConsumes("application/json"); + operation.Parameters.Add(new OpenApiParameter { Schema = entitySchemaRef, Kind = OpenApiParameterKind.Body }); operation.SetResponse(HttpStatusCode.OK, entitySchemaRef); operation.SetResponse(HttpStatusCode.BadRequest); operation.SetResponse(HttpStatusCode.NotFound); @@ -105,6 +110,41 @@ private static void ProcessDatasyncOperation(OperationProcessorContext context) } } + private static void AddMissingSchemaProperties(JsonSchema? schema) + { + if (schema is null) + { + return; + } + + if (schema.Properties.ContainsKey("id") && schema.Properties.ContainsKey("updatedAt") && schema.Properties.ContainsKey("version")) + { + // Nothing to do - the correct properties are already in the schma. + return; + } + + _ = schema.Properties.TryAdd("id", new JsonSchemaProperty + { + Type = JsonObjectType.String, + Description = "The globally unique ID for the entity", + IsRequired = true + }); + _ = schema.Properties.TryAdd("updatedAt", new JsonSchemaProperty + { + Type = JsonObjectType.String, + Description = "The ISO-8601 date/time string describing the last time the entity was updated with ms accuracy.", + IsRequired = false + }); + _ = schema.Properties.TryAdd("version", new JsonSchemaProperty + { + Type = JsonObjectType.String, + Description = "An opaque string that changes whenever the entity changes.", + IsRequired = false + }); + + return; + } + /// /// Either reads or generates the required entity type schema. /// From f000547664d6acf582a60c285cb7522d83bbc15d Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Thu, 3 Oct 2024 15:57:43 -0700 Subject: [PATCH 2/2] (#118) Updated expected swagger.json --- .../NSwag_Tests.cs | 2 +- .../swagger.json | 73 ++++++++++++++++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/tests/CommunityToolkit.Datasync.Server.NSwag.Test/NSwag_Tests.cs b/tests/CommunityToolkit.Datasync.Server.NSwag.Test/NSwag_Tests.cs index fa83b9db..ec3d5c41 100644 --- a/tests/CommunityToolkit.Datasync.Server.NSwag.Test/NSwag_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Server.NSwag.Test/NSwag_Tests.cs @@ -28,7 +28,7 @@ public async Task NSwag_GeneratesSwagger() // There is an x-generator field that is library specific and completely irrelevant // to the comparison, so this line will remove it for comparison purposes. - Regex generatorRegex = new("\"x-generator\": \"[^\\\"]+\","); + Regex generatorRegex = new("\"x-generator\": \"[^\\\"]+\",[\r\n]+"); actualContent = generatorRegex.Replace(actualContent, "", 1); expectedContent = generatorRegex.Replace(expectedContent, "", 1); diff --git a/tests/CommunityToolkit.Datasync.Server.NSwag.Test/swagger.json b/tests/CommunityToolkit.Datasync.Server.NSwag.Test/swagger.json index 0a378234..a2f6ebc4 100644 --- a/tests/CommunityToolkit.Datasync.Server.NSwag.Test/swagger.json +++ b/tests/CommunityToolkit.Datasync.Server.NSwag.Test/swagger.json @@ -1,6 +1,5 @@ { - - "openapi": "3.0.0", + "openapi": "3.0.0", "info": { "title": "My Title", "version": "1.0.0" @@ -223,6 +222,15 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KitchenSink" + } + } + } + }, "responses": { "201": { "description": "Created", @@ -576,6 +584,15 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KitchenSink" + } + } + } + }, "responses": { "200": { "description": "OK", @@ -660,6 +677,15 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoItem" + } + } + } + }, "responses": { "201": { "description": "Created", @@ -1013,6 +1039,15 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoItem" + } + } + } + }, "responses": { "200": { "description": "OK", @@ -1085,6 +1120,23 @@ "schemas": { "KitchenSink": { "title": "KitchenSink", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "The globally unique ID for the entity" + }, + "updatedAt": { + "type": "string", + "description": "The ISO-8601 date/time string describing the last time the entity was updated with ms accuracy." + }, + "version": { + "type": "string", + "description": "An opaque string that changes whenever the entity changes." + } + }, "definitions": { "KitchenSinkState": { "type": "integer", @@ -1246,6 +1298,23 @@ }, "TodoItem": { "title": "TodoItem", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "The globally unique ID for the entity" + }, + "updatedAt": { + "type": "string", + "description": "The ISO-8601 date/time string describing the last time the entity was updated with ms accuracy." + }, + "version": { + "type": "string", + "description": "An opaque string that changes whenever the entity changes." + } + }, "definitions": { "EntityTableData": { "allOf": [