diff --git a/sdk/dotnet/src/Microsoft.AspNetCore.Datasync.NSwag/DatasyncOperationProcessor.cs b/sdk/dotnet/src/Microsoft.AspNetCore.Datasync.NSwag/DatasyncOperationProcessor.cs
index af8d9ead..c13ce3fd 100644
--- a/sdk/dotnet/src/Microsoft.AspNetCore.Datasync.NSwag/DatasyncOperationProcessor.cs
+++ b/sdk/dotnet/src/Microsoft.AspNetCore.Datasync.NSwag/DatasyncOperationProcessor.cs
@@ -42,8 +42,7 @@ private static void ProcessDatasyncOperation(OperationProcessorContext context)
var path = context.OperationDescription.Path;
Type entityType = context.ControllerType.BaseType?.GetGenericArguments().FirstOrDefault()
?? throw new ArgumentException("Cannot process a non-generic table controller");
- var entitySchema = context.SchemaResolver.GetSchema(entityType, false);
- var entitySchemaRef = new JsonSchema { Reference = entitySchema };
+ JsonSchema entitySchemaRef = GetEntityReference(context, entityType);
operation.AddDatasyncRequestHeaders();
if (method.Equals("DELETE", StringComparison.InvariantCultureIgnoreCase))
@@ -94,6 +93,25 @@ private static void ProcessDatasyncOperation(OperationProcessorContext context)
}
}
+ ///
+ /// Either reads or generates the required entity type schema.
+ ///
+ /// The context for the operation processor.
+ /// The entity type needed.
+ /// A reference to the entity schema.
+ private static JsonSchema GetEntityReference(OperationProcessorContext context, Type entityType)
+ {
+ var schemaName = context.SchemaGenerator.Settings.SchemaNameGenerator.Generate(entityType);
+ if (!context.Document.Definitions.ContainsKey(schemaName))
+ {
+ var newSchema = context.SchemaGenerator.Generate(entityType);
+ context.Document.Definitions.Add(schemaName, newSchema);
+ }
+
+ var actualSchema = context.Document.Definitions[schemaName];
+ return new JsonSchema { Reference = actualSchema };
+ }
+
///
/// Creates the paged item schema reference.
///
diff --git a/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.NSwag.Test/NSwag_Tests.cs b/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.NSwag.Test/NSwag_Tests.cs
index 062b7a65..991d2893 100644
--- a/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.NSwag.Test/NSwag_Tests.cs
+++ b/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.NSwag.Test/NSwag_Tests.cs
@@ -16,14 +16,14 @@ private static string ReadExternalFile(string filename)
{
Assembly asm = Assembly.GetExecutingAssembly();
using Stream s = asm.GetManifestResourceStream(asm.GetName().Name + "." + filename)!;
- using StreamReader sr = new StreamReader(s);
+ using StreamReader sr = new(s);
return sr.ReadToEnd();
}
private static void WriteExternalFile(string filename, string content)
{
var storePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
- using StreamWriter outputFile = new StreamWriter(Path.Combine(storePath, filename));
+ using StreamWriter outputFile = new(Path.Combine(storePath, filename));
outputFile.Write(content);
}
@@ -40,6 +40,6 @@ public async Task NSwag_GeneratesSwagger()
{
WriteExternalFile("swagger.json.out", actualContent);
}
- Assert.Equal(expectedContent, actualContent.Replace("\r\n", "\n").TrimEnd());
+ Assert.Equal(expectedContent, actualContent);
}
}
\ No newline at end of file
diff --git a/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.NSwag.Test/Service/KitchenReaderController.cs b/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.NSwag.Test/Service/KitchenReaderController.cs
new file mode 100644
index 00000000..eaedbb6a
--- /dev/null
+++ b/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.NSwag.Test/Service/KitchenReaderController.cs
@@ -0,0 +1,45 @@
+using Microsoft.AspNetCore.Datasync.EFCore;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Datasync.NSwag.Test.Service;
+
+[ExcludeFromCodeCoverage]
+public abstract class ReadonlyTableController : TableController where TData : class, ITableData
+{
+ [NonAction]
+ public override Task CreateAsync([FromBody] TData item, CancellationToken token = default)
+ {
+ return base.CreateAsync(item, token);
+ }
+
+ [NonAction]
+ public override Task DeleteAsync([FromRoute] string id, CancellationToken token = default)
+ {
+ return base.DeleteAsync(id, token);
+ }
+
+ [NonAction]
+ public override Task PatchAsync([FromRoute] string id, CancellationToken token = default)
+ {
+ return base.PatchAsync(id, token);
+ }
+
+ [NonAction]
+ public override Task ReplaceAsync([FromRoute] string id, [FromBody] TData item, CancellationToken token = default)
+ {
+ return base.ReplaceAsync(id, item, token);
+ }
+}
+
+[Route("tables/kitchenreader")]
+[ExcludeFromCodeCoverage]
+public class KitchenReaderController : ReadonlyTableController
+{
+ public KitchenReaderController(ServiceDbContext context, ILogger logger) : base()
+ {
+ Repository = new EntityTableRepository(context);
+ Logger = logger;
+ }
+}
+
diff --git a/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.NSwag.Test/swagger.json b/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.NSwag.Test/swagger.json
index d85fd736..0bbce2a4 100644
--- a/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.NSwag.Test/swagger.json
+++ b/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.NSwag.Test/swagger.json
@@ -11,6 +11,224 @@
}
],
"paths": {
+ "/tables/kitchenreader": {
+ "get": {
+ "tags": [
+ "KitchenReader"
+ ],
+ "operationId": "KitchenReader_Query",
+ "parameters": [
+ {
+ "name": "ZUMO-API-VERSION",
+ "in": "header",
+ "required": true,
+ "description": "Sets the Datasync API version to use.",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "3.0.0",
+ "2.0.0"
+ ]
+ }
+ },
+ {
+ "name": "$count",
+ "in": "query",
+ "description": "If true, return the total number of items matched by the filter",
+ "schema": {
+ "type": "boolean"
+ }
+ },
+ {
+ "name": "$filter",
+ "in": "query",
+ "description": "An OData filter describing the entities to be returned",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "$orderby",
+ "in": "query",
+ "description": "A comma-separated list of ordering instructions. Each ordering instruction is a field name with an optional direction (asc or desc).",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "$select",
+ "in": "query",
+ "description": "A comma-separated list of fields to be returned in the result set.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "$skip",
+ "in": "query",
+ "description": "The number of items in the list to skip for paging support.",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "$top",
+ "in": "query",
+ "description": "The number of items in the list to return for paging support.",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "__includedeleted",
+ "in": "query",
+ "description": "If true, soft-deleted items are returned as well as non-deleted items.",
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "description": "A page of KitchenSink entities",
+ "properties": {
+ "items": {
+ "type": "array",
+ "readOnly": true,
+ "description": "The entities in this page of results",
+ "nullable": true,
+ "items": {
+ "$ref": "#/components/schemas/KitchenSink"
+ }
+ },
+ "count": {
+ "type": "integer",
+ "readOnly": true,
+ "description": "The count of all entities in the result set",
+ "nullable": true
+ },
+ "nextLink": {
+ "type": "string",
+ "readOnly": true,
+ "description": "The URI to the next page of entities",
+ "format": "uri",
+ "nullable": true
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request"
+ }
+ }
+ }
+ },
+ "/tables/kitchenreader/{id}": {
+ "get": {
+ "tags": [
+ "KitchenReader"
+ ],
+ "operationId": "KitchenReader_Read",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "x-position": 1
+ },
+ {
+ "name": "ZUMO-API-VERSION",
+ "in": "header",
+ "required": true,
+ "description": "Sets the Datasync API version to use.",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "3.0.0",
+ "2.0.0"
+ ]
+ }
+ },
+ {
+ "name": "If-None-Match",
+ "in": "header",
+ "description": "Conditionally execute only if the entity version does not match the provided string (RFC 9110 13.1.2).",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "headers": {
+ "ETag": {
+ "description": "The version string of the server entity, per RFC 9110",
+ "schema": {
+ "type": "string"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/KitchenSink"
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Conflict",
+ "headers": {
+ "ETag": {
+ "description": "The version string of the server entity, per RFC 9110",
+ "schema": {
+ "type": "string"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/KitchenSink"
+ }
+ }
+ }
+ },
+ "412": {
+ "description": "Precondition Failed",
+ "headers": {
+ "ETag": {
+ "description": "The version string of the server entity, per RFC 9110",
+ "schema": {
+ "type": "string"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/KitchenSink"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found"
+ }
+ }
+ }
+ },
"/tables/kitchensink": {
"post": {
"tags": [
@@ -45,7 +263,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/KitchenSink"
+ "$ref": "#/components/schemas/KitchenSink2"
}
}
},
@@ -558,7 +776,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/KitchenSink"
+ "$ref": "#/components/schemas/KitchenSink2"
}
}
},
@@ -1257,6 +1475,93 @@
"components": {
"schemas": {
"KitchenSink": {
+ "title": "KitchenSink",
+ "definitions": {
+ "EntityTableData": {
+ "type": "object",
+ "x-abstract": true,
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "nullable": true
+ },
+ "updatedAt": {
+ "type": "string",
+ "readOnly": true,
+ "format": "date-time"
+ },
+ "version": {
+ "type": "string",
+ "readOnly": true,
+ "format": "byte",
+ "nullable": true
+ },
+ "deleted": {
+ "type": "boolean",
+ "readOnly": true
+ }
+ }
+ }
+ },
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/KitchenSink/definitions/EntityTableData"
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "booleanValue": {
+ "type": "boolean"
+ },
+ "intValue": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "longValue": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "decimalValue": {
+ "type": "number",
+ "format": "decimal"
+ },
+ "doubleValue": {
+ "type": "number",
+ "format": "double"
+ },
+ "floatValue": {
+ "type": "number",
+ "format": "float"
+ },
+ "charValue": {
+ "type": "string"
+ },
+ "stringValue": {
+ "type": "string",
+ "nullable": true
+ },
+ "dateTimeValue": {
+ "type": "string",
+ "format": "date-time",
+ "nullable": true
+ },
+ "dateTimeOffsetValue": {
+ "type": "string",
+ "format": "date-time",
+ "nullable": true
+ },
+ "guidValue": {
+ "type": "string",
+ "format": "guid",
+ "nullable": true
+ }
+ }
+ }
+ ]
+ },
+ "KitchenSink2": {
"allOf": [
{
"$ref": "#/components/schemas/EntityTableData"
@@ -1359,4 +1664,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.Swashbuckle.Test/Service/KitchenReaderController.cs b/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.Swashbuckle.Test/Service/KitchenReaderController.cs
new file mode 100644
index 00000000..5c369253
--- /dev/null
+++ b/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.Swashbuckle.Test/Service/KitchenReaderController.cs
@@ -0,0 +1,45 @@
+using Microsoft.AspNetCore.Datasync.EFCore;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.Datasync.Swashbuckle.Test.Service
+{
+ [ExcludeFromCodeCoverage]
+ public abstract class ReadonlyTableController : TableController where TData : class, ITableData
+ {
+ [NonAction]
+ public override Task CreateAsync([FromBody] TData item, CancellationToken token = default)
+ {
+ return base.CreateAsync(item, token);
+ }
+
+ [NonAction]
+ public override Task DeleteAsync([FromRoute] string id, CancellationToken token = default)
+ {
+ return base.DeleteAsync(id, token);
+ }
+
+ [NonAction]
+ public override Task PatchAsync([FromRoute] string id, CancellationToken token = default)
+ {
+ return base.PatchAsync(id, token);
+ }
+
+ [NonAction]
+ public override Task ReplaceAsync([FromRoute] string id, [FromBody] TData item, CancellationToken token = default)
+ {
+ return base.ReplaceAsync(id, item, token);
+ }
+ }
+
+ [Route("tables/kitchenreader")]
+ [ExcludeFromCodeCoverage]
+ public class KitchenReaderController : ReadonlyTableController
+ {
+ public KitchenReaderController(ServiceDbContext context, ILogger logger) : base()
+ {
+ Repository = new EntityTableRepository(context);
+ Logger = logger;
+ }
+ }
+}
diff --git a/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.Swashbuckle.Test/SwaggerGen_Tests.cs b/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.Swashbuckle.Test/SwaggerGen_Tests.cs
index 5ae9b290..aaa486ad 100644
--- a/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.Swashbuckle.Test/SwaggerGen_Tests.cs
+++ b/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.Swashbuckle.Test/SwaggerGen_Tests.cs
@@ -19,6 +19,13 @@ private static string ReadExternalFile(string filename)
return sr.ReadToEnd();
}
+ private static void WriteExternalFile(string filename, string content)
+ {
+ var storePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ using StreamWriter outputFile = new(Path.Combine(storePath, filename));
+ outputFile.Write(content);
+ }
+
[Fact]
public void DocumentFilter_ReadsAllControllers()
{
@@ -27,7 +34,8 @@ public void DocumentFilter_ReadsAllControllers()
var controllers = DatasyncDocumentFilter.GetAllTableControllers().Select(m => m.Name).ToList();
// There should be two controllers
- Assert.Equal(2, controllers.Count);
+ Assert.Equal(3, controllers.Count);
+ Assert.Contains("KitchenReaderController", controllers);
Assert.Contains("KitchenSinkController", controllers);
Assert.Contains("TodoItemController", controllers);
}
@@ -40,7 +48,11 @@ public async Task SwaggerGen_GeneratesSwagger()
Assert.True(swaggerDoc!.IsSuccessStatusCode);
var expectedContent = ReadExternalFile("swagger.json").Replace("\r\n", "\n").TrimEnd();
- var actualContent = await swaggerDoc!.Content.ReadAsStringAsync();
- Assert.Equal(expectedContent, actualContent.Replace("\r\n", "\n").TrimEnd());
+ var actualContent = (await swaggerDoc!.Content.ReadAsStringAsync()).Replace("\r\n", "\n").TrimEnd();
+ if (!expectedContent.Equals(actualContent))
+ {
+ WriteExternalFile("swagger.json.out", actualContent);
+ }
+ Assert.Equal(expectedContent, actualContent);
}
}
diff --git a/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.Swashbuckle.Test/swagger.json b/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.Swashbuckle.Test/swagger.json
index ec1f1b2d..2b189ff5 100644
--- a/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.Swashbuckle.Test/swagger.json
+++ b/sdk/dotnet/test/Microsoft.AspNetCore.Datasync.Swashbuckle.Test/swagger.json
@@ -5,6 +5,40 @@
"version": "1.0"
},
"paths": {
+ "/tables/kitchenreader": {
+ "get": {
+ "tags": [
+ "KitchenReader"
+ ],
+ "responses": {
+ "200": {
+ "description": "Success"
+ }
+ }
+ }
+ },
+ "/tables/kitchenreader/{id}": {
+ "get": {
+ "tags": [
+ "KitchenReader"
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success"
+ }
+ }
+ }
+ },
"/tables/kitchensink": {
"post": {
"tags": [
@@ -1271,4 +1305,4 @@
}
}
}
-}
\ No newline at end of file
+}