Skip to content

Commit f06016c

Browse files
author
Adrian Hall
committed
(#48) Interim support for .NET 9 OpenApi. Does not handle schema generation.
1 parent aae225b commit f06016c

30 files changed

+2125
-322
lines changed

Datasync.Toolkit.sln

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Datasync.S
7070
EndProject
7171
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Datasync.Server.OpenApi.Test", "tests\CommunityToolkit.Datasync.Server.OpenApi.Test\CommunityToolkit.Datasync.Server.OpenApi.Test.csproj", "{99E727A3-8EB3-437E-AAC8-3596E8B9B2AE}"
7272
EndProject
73-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Datasync.OpenApiService", "tests\CommunityToolkit.Datasync.OpenApiService\CommunityToolkit.Datasync.OpenApiService.csproj", "{ED03E99D-58BC-48C1-963B-0F02E28FCEDD}"
73+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Datasync.Server.OpenApi", "src\CommunityToolkit.Datasync.Server.OpenApi\CommunityToolkit.Datasync.Server.OpenApi.csproj", "{505421EC-2598-4114-B2EC-997959B89E74}"
7474
EndProject
7575
Global
7676
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -174,10 +174,10 @@ Global
174174
{99E727A3-8EB3-437E-AAC8-3596E8B9B2AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
175175
{99E727A3-8EB3-437E-AAC8-3596E8B9B2AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
176176
{99E727A3-8EB3-437E-AAC8-3596E8B9B2AE}.Release|Any CPU.Build.0 = Release|Any CPU
177-
{ED03E99D-58BC-48C1-963B-0F02E28FCEDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
178-
{ED03E99D-58BC-48C1-963B-0F02E28FCEDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
179-
{ED03E99D-58BC-48C1-963B-0F02E28FCEDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
180-
{ED03E99D-58BC-48C1-963B-0F02E28FCEDD}.Release|Any CPU.Build.0 = Release|Any CPU
177+
{505421EC-2598-4114-B2EC-997959B89E74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
178+
{505421EC-2598-4114-B2EC-997959B89E74}.Debug|Any CPU.Build.0 = Debug|Any CPU
179+
{505421EC-2598-4114-B2EC-997959B89E74}.Release|Any CPU.ActiveCfg = Release|Any CPU
180+
{505421EC-2598-4114-B2EC-997959B89E74}.Release|Any CPU.Build.0 = Release|Any CPU
181181
EndGlobalSection
182182
GlobalSection(SolutionProperties) = preSolution
183183
HideSolutionNode = FALSE
@@ -207,7 +207,7 @@ Global
207207
{DC20ACF9-12E9-41D9-B672-CB5FD85548E9} = {84AD662A-4B9E-4E64-834D-72529FB7FCE5}
208208
{4FC45D20-0BA9-484B-9040-641687659AF6} = {D59F1489-5D74-4F52-B78B-88037EAB2838}
209209
{99E727A3-8EB3-437E-AAC8-3596E8B9B2AE} = {D59F1489-5D74-4F52-B78B-88037EAB2838}
210-
{ED03E99D-58BC-48C1-963B-0F02E28FCEDD} = {D59F1489-5D74-4F52-B78B-88037EAB2838}
210+
{505421EC-2598-4114-B2EC-997959B89E74} = {84AD662A-4B9E-4E64-834D-72529FB7FCE5}
211211
EndGlobalSection
212212
GlobalSection(ExtensibilityGlobals) = postSolution
213213
SolutionGuid = {78A935E9-8F14-448A-BEDF-360FB742F14E}

samples/datasync-server/src/Sample.Datasync.Server/appsettings.Development.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
}
77
},
88
"Swagger": {
9-
"Driver": "Swashbuckle"
9+
"Driver": "net9"
1010
}
1111
}

samples/datasync-server/src/Sample.Datasync.Server/appsettings.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,5 @@
88
"Microsoft.AspNetCore": "Warning"
99
}
1010
},
11-
"AllowedHosts": "*",
12-
"Swagger": {
13-
"Driver": "net9"
14-
}
11+
"AllowedHosts": "*"
1512
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<Description>Provides necessary capabilities for supporting the Datasync server library when using the official OpenApi generator.</Description>
4+
</PropertyGroup>
5+
6+
<ItemGroup>
7+
<InternalsVisibleTo Include="CommunityToolkit.Datasync.Server.OpenApi.Test" />
8+
</ItemGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\CommunityToolkit.Datasync.Server\CommunityToolkit.Datasync.Server.csproj" />
16+
</ItemGroup>
17+
</Project>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.AspNetCore.OpenApi;
6+
using Microsoft.OpenApi.Models;
7+
8+
namespace CommunityToolkit.Datasync.Server.OpenApi;
9+
10+
/// <summary>
11+
/// The document transformer for the Datasync services.
12+
/// </summary>
13+
public class DatasyncDocumentTransformer : IOpenApiDocumentTransformer
14+
{
15+
/// <inheritdoc />
16+
public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
17+
{
18+
return Task.CompletedTask;
19+
}
20+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using CommunityToolkit.Datasync.Server.Filters;
6+
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.Mvc.Controllers;
8+
using Microsoft.AspNetCore.OpenApi;
9+
using Microsoft.OpenApi.Models;
10+
11+
namespace CommunityToolkit.Datasync.Server.OpenApi;
12+
13+
/// <summary>
14+
/// The document transformer for the Datasync services.
15+
/// </summary>
16+
public class DatasyncOperationTransformer : IOpenApiOperationTransformer
17+
{
18+
/// <inheritdoc />
19+
public async Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
20+
{
21+
// Determine if this operation is in scope.
22+
if (!IsDatasyncController(context))
23+
{
24+
return;
25+
}
26+
27+
// Ensure the operation is set up for modification.
28+
operation.Parameters ??= [];
29+
30+
string? actionName = context.Description.ActionDescriptor.RouteValues["action"];
31+
if (actionName?.StartsWith("Create", StringComparison.InvariantCultureIgnoreCase) == true)
32+
{
33+
await TransformCreateAsync(operation, context, cancellationToken).ConfigureAwait(false);
34+
return;
35+
}
36+
37+
if (actionName?.StartsWith("Delete", StringComparison.InvariantCultureIgnoreCase) == true)
38+
{
39+
await TransformDeleteAsync(operation, context, cancellationToken).ConfigureAwait(false);
40+
return;
41+
}
42+
43+
if (actionName?.StartsWith("Query", StringComparison.InvariantCultureIgnoreCase) == true)
44+
{
45+
await TransformQueryAsync(operation, context, cancellationToken).ConfigureAwait(false);
46+
return;
47+
}
48+
49+
if (actionName?.StartsWith("Read", StringComparison.InvariantCultureIgnoreCase) == true)
50+
{
51+
await TransformReadAsync(operation, context, cancellationToken).ConfigureAwait(false);
52+
return;
53+
}
54+
55+
return;
56+
}
57+
58+
/// <summary>
59+
/// Determines if a controller presented is a datasync controller.
60+
/// </summary>
61+
/// <param name="context">The transformer context.</param>
62+
/// <returns>true if the controller is a datasync controller; false otherwise.</returns>
63+
internal static bool IsDatasyncController(OpenApiOperationTransformerContext context)
64+
=> context.Description.ActionDescriptor.FilterDescriptors.Any(fd => fd.Filter is DatasyncControllerAttribute);
65+
66+
/// <summary>
67+
/// Retrieves the entity type for the controller.
68+
/// </summary>
69+
/// <param name="context">The transformer context.</param>
70+
/// <returns>The type of the entity being served.</returns>
71+
internal static Type GetEntityType(OpenApiOperationTransformerContext context)
72+
{
73+
Type? baseType = (context.Description.ActionDescriptor as ControllerActionDescriptor)?.ControllerTypeInfo.AsType();
74+
while (baseType is not null)
75+
{
76+
if (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == typeof(TableController<>))
77+
{
78+
Type? entityType = baseType.GetGenericArguments().FirstOrDefault();
79+
if (entityType is not null && typeof(ITableData).IsAssignableFrom(entityType))
80+
{
81+
return entityType;
82+
}
83+
84+
throw new InvalidOperationException("Expecting the entity type to implement ITableData.");
85+
}
86+
87+
baseType = baseType.BaseType;
88+
}
89+
90+
throw new InvalidOperationException("Expecting the controller to be derived from TableController<T>.");
91+
}
92+
93+
/// <summary>
94+
/// Transforms a create operation.
95+
/// </summary>
96+
/// <param name="operation">The operation to transform.</param>
97+
/// <param name="context">The operation transformer context.</param>
98+
/// <param name="cancellationToken">A cancellation token to observe.</param>
99+
/// <returns>A task that resolves when the operation is complete.</returns>
100+
internal Task TransformCreateAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
101+
{
102+
Type entityType = GetEntityType(context);
103+
104+
operation.Responses.AddEntityResponse(201, context.GetSchemaForType(entityType), includeConditionalHeaders: true);
105+
operation.Responses.AddStatusCode(StatusCodes.Status400BadRequest);
106+
operation.Responses.AddEntityResponse(409, context.GetSchemaForType(entityType), includeConditionalHeaders: true);
107+
operation.Responses.AddEntityResponse(412, context.GetSchemaForType(entityType), includeConditionalHeaders: true);
108+
109+
return Task.CompletedTask;
110+
}
111+
112+
/// <summary>
113+
/// Transforms a delete operation.
114+
/// </summary>
115+
/// <param name="operation">The operation to transform.</param>
116+
/// <param name="context">The operation transformer context.</param>
117+
/// <param name="cancellationToken">A cancellation token to observe.</param>
118+
/// <returns>A task that resolves when the operation is complete.</returns>
119+
internal Task TransformDeleteAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
120+
{
121+
Type entityType = GetEntityType(context);
122+
123+
operation.Parameters.AddIfMatchHeader();
124+
operation.Parameters.AddIfUnmodifiedSinceHeader();
125+
126+
operation.Responses.AddStatusCode(StatusCodes.Status400BadRequest);
127+
operation.Responses.AddStatusCode(StatusCodes.Status404NotFound);
128+
operation.Responses.AddStatusCode(StatusCodes.Status410Gone);
129+
130+
operation.Responses.AddEntityResponse(409, context.GetSchemaForType(entityType), includeConditionalHeaders: true);
131+
operation.Responses.AddEntityResponse(412, context.GetSchemaForType(entityType), includeConditionalHeaders: true);
132+
133+
return Task.CompletedTask;
134+
}
135+
136+
/// <summary>
137+
/// Transforms a query operation.
138+
/// </summary>
139+
/// <param name="operation">The operation to transform.</param>
140+
/// <param name="context">The operation transformer context.</param>
141+
/// <param name="cancellationToken">A cancellation token to observe.</param>
142+
/// <returns>A task that resolves when the operation is complete.</returns>
143+
internal Task TransformQueryAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
144+
{
145+
Type entityType = GetEntityType(context);
146+
Type pagedEntityType = typeof(PagedResult<>).MakeGenericType(entityType);
147+
148+
operation.Parameters.AddBooleanQueryParameter("$count", "Whether to include the total count of items matching the query in the result");
149+
operation.Parameters.AddStringQueryParameter("$filter", "The filter to apply to the query");
150+
operation.Parameters.AddStringQueryParameter("$orderby", "The comma-separated list of ordering instructions to apply to the query");
151+
operation.Parameters.AddStringQueryParameter("$select", "The comma-separated list of fields to return in the results");
152+
operation.Parameters.AddIntQueryParameter("$skip", "The number of items to skip", 0);
153+
operation.Parameters.AddIntQueryParameter("$top", "The number of items to return", 1);
154+
operation.Parameters.AddIncludeDeletedQuery();
155+
156+
operation.Responses.AddEntityResponse(200, context.GetSchemaForType(pagedEntityType), includeConditionalHeaders: false);
157+
operation.Responses.AddStatusCode(StatusCodes.Status400BadRequest);
158+
159+
return Task.CompletedTask;
160+
}
161+
162+
/// <summary>
163+
/// Transforms a read operation.
164+
/// </summary>
165+
/// <param name="operation">The operation to transform.</param>
166+
/// <param name="context">The operation transformer context.</param>
167+
/// <param name="cancellationToken">A cancellation token to observe.</param>
168+
/// <returns>A task that resolves when the operation is complete.</returns>
169+
internal Task TransformReadAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
170+
{
171+
Type entityType = GetEntityType(context);
172+
173+
operation.Parameters.AddIncludeDeletedQuery();
174+
operation.Parameters.AddIfNoneMatchHeader();
175+
operation.Parameters.AddIfModifiedSinceHeader();
176+
177+
operation.Responses.AddEntityResponse(200, context.GetSchemaForType(entityType), includeConditionalHeaders: true);
178+
operation.Responses.AddStatusCode(StatusCodes.Status304NotModified);
179+
operation.Responses.AddStatusCode(StatusCodes.Status404NotFound);
180+
operation.Responses.AddStatusCode(StatusCodes.Status410Gone);
181+
182+
return Task.CompletedTask;
183+
}
184+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.AspNetCore.OpenApi;
6+
using Microsoft.OpenApi.Models;
7+
8+
namespace CommunityToolkit.Datasync.Server.OpenApi;
9+
10+
/// <summary>
11+
/// The document transformer for the Datasync services.
12+
/// </summary>
13+
public class DatasyncSchemaTransformer : IOpenApiSchemaTransformer
14+
{
15+
/// <inheritdoc />
16+
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
17+
{
18+
return Task.CompletedTask;
19+
}
20+
}

0 commit comments

Comments
 (0)