Skip to content

Commit cd43e27

Browse files
Eliminate duplicated request bodies and responses for actions/functions (#266)
* Eliminate duplicated request bodies for actions * Eliminate duplicated responses for actions and functions * Update tests * Update release notes * Formatting fixes * Update XML comment of method * Use pattern matching in if statement
1 parent cd5b900 commit cd43e27

18 files changed

+581
-1208
lines changed

src/Microsoft.OpenApi.OData.Reader/Edm/EdmModelExtensions.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,28 @@ public static bool IsOperationOverload(this IEdmModel model, IEdmOperation opera
200200
}
201201

202202
/// <summary>
203-
/// Check whether the operaiton import is overload in the model.
203+
/// Checks whether operation targets singletons and/or entitysets of the same type.
204+
/// </summary>
205+
/// <param name="model">The Edm model.</param>
206+
/// <param name="operation">The test operations.</param>
207+
/// <returns>True/false.</returns>
208+
public static bool OperationTargetsMultiplePaths(this IEdmModel model, IEdmOperation operation)
209+
{
210+
Utils.CheckArgumentNull(model, nameof(model));
211+
Utils.CheckArgumentNull(operation, nameof(operation));
212+
213+
if (!operation.Parameters.Any())
214+
return false;
215+
216+
IEdmTypeReference bindingParameterType = operation.Parameters.First().Type;
217+
218+
return model.EntityContainer.EntitySets().Select(static x => x.EntityType())
219+
.Concat(model.EntityContainer.Singletons().Select(static x => x.EntityType()))
220+
.Where(x => x.FullName().Equals(bindingParameterType.FullName(), StringComparison.OrdinalIgnoreCase)).Count() > 1;
221+
}
222+
223+
/// <summary>
224+
/// Check whether the operation import is overload in the model.
204225
/// </summary>
205226
/// <param name="model">The Edm model.</param>
206227
/// <param name="operationImport">The test operations.</param>

src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiRequestBodyGenerator.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,28 @@ public static IDictionary<string, OpenApiRequestBody> CreateRequestBodies(this O
9595
{
9696
Utils.CheckArgumentNull(context, nameof(context));
9797

98-
return new Dictionary<string, OpenApiRequestBody>
98+
Dictionary<string, OpenApiRequestBody> requestBodies = new()
9999
{
100100
{
101101
Constants.ReferencePostRequestBodyName,
102-
CreateRefPostRequestBody()
102+
CreateRefPostRequestBody()
103103
},
104104
{
105105
Constants.ReferencePutRequestBodyName,
106106
CreateRefPutRequestBody()
107107
}
108108
};
109+
110+
// add request bodies for actions targeting multiple related paths
111+
foreach (IEdmAction action in context.Model.SchemaElements.OfType<IEdmAction>()
112+
.Where(action => context.Model.OperationTargetsMultiplePaths(action)))
113+
{
114+
OpenApiRequestBody requestBody = context.CreateRequestBody(action);
115+
if (requestBody != null)
116+
requestBodies.Add($"{action.Name}RequestBody", requestBody);
117+
}
118+
119+
return requestBodies;
109120
}
110121

111122
/// <summary>

src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiResponseGenerator.cs

Lines changed: 89 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ public static IDictionary<string, OpenApiResponse> CreateResponses(this ODataCon
107107
if(context.HasAnyNonContainedCollections())
108108
responses[$"String{Constants.CollectionSchemaSuffix}"] = CreateCollectionResponse("String");
109109

110+
foreach (IEdmOperation operation in context.Model.SchemaElements.OfType<IEdmOperation>()
111+
.Where(op => context.Model.OperationTargetsMultiplePaths(op)))
112+
{
113+
OpenApiResponse response = context.CreateOperationResponse(operation);
114+
if (response != null)
115+
responses[$"{operation.Name}Response"] = response;
116+
}
117+
110118
return responses;
111119
}
112120

@@ -115,110 +123,130 @@ public static IDictionary<string, OpenApiResponse> CreateResponses(this ODataCon
115123
/// </summary>
116124
/// <param name="context">The OData context.</param>
117125
/// <param name="operationImport">The Edm operation import.</param>
118-
/// <param name="path">The OData path.</param>
119126
/// <returns>The created <see cref="OpenApiResponses"/>.</returns>
120-
public static OpenApiResponses CreateResponses(this ODataContext context, IEdmOperationImport operationImport, ODataPath path)
127+
public static OpenApiResponses CreateResponses(this ODataContext context, IEdmOperationImport operationImport)
121128
{
122129
Utils.CheckArgumentNull(context, nameof(context));
123130
Utils.CheckArgumentNull(operationImport, nameof(operationImport));
124-
Utils.CheckArgumentNull(path, nameof(path));
125131

126-
return context.CreateResponses(operationImport.Operation, path);
132+
return context.CreateResponses(operationImport.Operation);
127133
}
128134

129135
/// <summary>
130136
/// Create the <see cref="OpenApiResponses"/> for a <see cref="IEdmOperation"/>
131137
/// </summary>
132138
/// <param name="context">The OData context.</param>
133139
/// <param name="operation">The Edm operation.</param>
134-
/// <param name="path">The OData path.</param>
135140
/// <returns>The created <see cref="OpenApiResponses"/>.</returns>
136-
public static OpenApiResponses CreateResponses(this ODataContext context, IEdmOperation operation, ODataPath path)
141+
public static OpenApiResponses CreateResponses(this ODataContext context, IEdmOperation operation)
137142
{
138143
Utils.CheckArgumentNull(context, nameof(context));
139144
Utils.CheckArgumentNull(operation, nameof(operation));
140-
Utils.CheckArgumentNull(path, nameof(path));
141145

142146
OpenApiResponses responses = new();
143147

144148
if (operation.IsAction() && operation.ReturnType == null)
145149
{
146150
responses.Add(Constants.StatusCode204, Constants.StatusCode204.GetResponse());
147151
}
148-
else
152+
else if (context.Model.OperationTargetsMultiplePaths(operation))
149153
{
150-
OpenApiSchema schema;
151-
if (operation.ReturnType.IsCollection())
152-
{
153-
// Get the entity type of the previous segment
154-
IEdmEntityType entityType = path.Segments.Reverse().Skip(1)?.Take(1)?.FirstOrDefault()?.EntityType;
155-
schema = new OpenApiSchema
154+
responses.Add(
155+
context.Settings.UseSuccessStatusCodeRange ? Constants.StatusCodeClass2XX : Constants.StatusCode200,
156+
new OpenApiResponse
156157
{
157-
Title = entityType == null ? null : $"Collection of {entityType.Name}",
158-
Type = "object",
159-
Properties = new Dictionary<string, OpenApiSchema>
158+
UnresolvedReference = true,
159+
Reference = new OpenApiReference()
160160
{
161-
{
162-
"value", context.CreateEdmTypeSchema(operation.ReturnType)
163-
}
161+
Type = ReferenceType.Response,
162+
Id = $"{operation.Name}Response"
164163
}
165-
};
166-
}
167-
else if (operation.ReturnType.IsPrimitive())
164+
}
165+
);
166+
}
167+
else
168+
{
169+
OpenApiResponse response = context.CreateOperationResponse(operation);
170+
responses.Add(context.Settings.UseSuccessStatusCodeRange ? Constants.StatusCodeClass2XX : Constants.StatusCode200, response);
171+
}
172+
173+
if (context.Settings.ErrorResponsesAsDefault)
174+
{
175+
responses.Add(Constants.StatusCodeDefault, Constants.StatusCodeDefault.GetResponse());
176+
}
177+
else
178+
{
179+
responses.Add(Constants.StatusCodeClass4XX, Constants.StatusCodeClass4XX.GetResponse());
180+
responses.Add(Constants.StatusCodeClass5XX, Constants.StatusCodeClass5XX.GetResponse());
181+
}
182+
183+
return responses;
184+
}
185+
186+
public static OpenApiResponse CreateOperationResponse(this ODataContext context, IEdmOperation operation)
187+
{
188+
if (operation.ReturnType == null)
189+
return null;
190+
191+
OpenApiSchema schema;
192+
if (operation.ReturnType.IsCollection())
193+
{
194+
schema = new OpenApiSchema
168195
{
169-
// A property or operation response that is of a primitive type is represented as an object with a single name/value pair,
170-
// whose name is value and whose value is a primitive value.
171-
schema = new OpenApiSchema
196+
Title = operation.ReturnType.Definition.AsElementType() is not IEdmEntityType entityType
197+
? null : $"Collection of {entityType.Name}",
198+
Type = "object",
199+
Properties = new Dictionary<string, OpenApiSchema>
172200
{
173-
Type = "object",
174-
Properties = new Dictionary<string, OpenApiSchema>
175201
{
176-
{
177-
"value", context.CreateEdmTypeSchema(operation.ReturnType)
178-
}
202+
"value", context.CreateEdmTypeSchema(operation.ReturnType)
179203
}
180-
};
181-
}
182-
else
183-
{
184-
schema = context.CreateEdmTypeSchema(operation.ReturnType);
185-
}
186-
187-
string mediaType = Constants.ApplicationJsonMediaType;
188-
if (operation.ReturnType.AsPrimitive()?.PrimitiveKind() == EdmPrimitiveTypeKind.Stream)
189-
{
190-
// Responses of types Edm.Stream should be application/octet-stream
191-
mediaType = Constants.ApplicationOctetStreamMediaType;
192-
}
193-
194-
OpenApiResponse response = new()
204+
}
205+
};
206+
}
207+
else if (operation.ReturnType.IsPrimitive())
208+
{
209+
// A property or operation response that is of a primitive type is represented as an object with a single name/value pair,
210+
// whose name is value and whose value is a primitive value.
211+
schema = new OpenApiSchema
195212
{
196-
Description = "Success",
197-
Content = new Dictionary<string, OpenApiMediaType>
213+
Type = "object",
214+
Properties = new Dictionary<string, OpenApiSchema>
198215
{
199216
{
200-
mediaType,
201-
new OpenApiMediaType
202-
{
203-
Schema = schema
204-
}
217+
"value", context.CreateEdmTypeSchema(operation.ReturnType)
205218
}
206219
}
207220
};
208-
responses.Add(context.Settings.UseSuccessStatusCodeRange ? Constants.StatusCodeClass2XX : Constants.StatusCode200, response);
209221
}
210-
211-
if (context.Settings.ErrorResponsesAsDefault)
222+
else
212223
{
213-
responses.Add(Constants.StatusCodeDefault, Constants.StatusCodeDefault.GetResponse());
224+
schema = context.CreateEdmTypeSchema(operation.ReturnType);
214225
}
215-
else
226+
227+
string mediaType = Constants.ApplicationJsonMediaType;
228+
if (operation.ReturnType.AsPrimitive()?.PrimitiveKind() == EdmPrimitiveTypeKind.Stream)
216229
{
217-
responses.Add(Constants.StatusCodeClass4XX, Constants.StatusCodeClass4XX.GetResponse());
218-
responses.Add(Constants.StatusCodeClass5XX, Constants.StatusCodeClass5XX.GetResponse());
230+
// Responses of types Edm.Stream should be application/octet-stream
231+
mediaType = Constants.ApplicationOctetStreamMediaType;
219232
}
220233

221-
return responses;
234+
OpenApiResponse response = new()
235+
{
236+
Description = "Success",
237+
Content = new Dictionary<string, OpenApiMediaType>
238+
{
239+
{
240+
mediaType,
241+
new OpenApiMediaType
242+
{
243+
Schema = schema
244+
}
245+
}
246+
}
247+
};
248+
249+
return response;
222250
}
223251

224252
private static OpenApiResponse CreateCollectionResponse(IEdmStructuredType structuredType)

src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<RepositoryUrl>https://github.com/Microsoft/OpenAPI.NET.OData</RepositoryUrl>
2323
<PackageReleaseNotes>
2424
- Fixes response schemas of actions and functions that return a collection to contain the nextLink property #231
25+
- Fixes duplicated actions/functions request body/response schemas #241
2526
</PackageReleaseNotes>
2627
<AssemblyName>Microsoft.OpenApi.OData.Reader</AssemblyName>
2728
<AssemblyOriginatorKeyFile>..\..\tool\Microsoft.OpenApi.OData.snk</AssemblyOriginatorKeyFile>

src/Microsoft.OpenApi.OData.Reader/Operation/EdmActionOperationHandler.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,24 @@ protected override void SetBasicInfo(OpenApiOperation operation)
3737
/// <inheritdoc/>
3838
protected override void SetRequestBody(OpenApiOperation operation)
3939
{
40-
IEdmAction action = EdmOperation as IEdmAction;
41-
if (action != null)
42-
{
43-
operation.RequestBody = Context.CreateRequestBody(action);
40+
if (EdmOperation is IEdmAction action && Context.CreateRequestBody(action) is OpenApiRequestBody requestBody)
41+
{
42+
if (Context.Model.OperationTargetsMultiplePaths(action))
43+
{
44+
operation.RequestBody = new OpenApiRequestBody
45+
{
46+
UnresolvedReference = true,
47+
Reference = new OpenApiReference
48+
{
49+
Type = ReferenceType.RequestBody,
50+
Id = $"{action.Name}RequestBody"
51+
}
52+
};
53+
}
54+
else
55+
{
56+
operation.RequestBody = requestBody;
57+
}
4458
}
4559

4660
base.SetRequestBody(operation);

src/Microsoft.OpenApi.OData.Reader/Operation/EdmOperationImportOperationHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ protected override void SetResponses(OpenApiOperation operation)
7777
// describing the structure of the success response by referencing an appropriate schema
7878
// in the global schemas. In addition, it contains a default name/value pair for
7979
// the OData error response referencing the global responses.
80-
operation.Responses = Context.CreateResponses(EdmOperationImport, Path);
80+
operation.Responses = Context.CreateResponses(EdmOperationImport);
8181

8282
base.SetResponses(operation);
8383
}

src/Microsoft.OpenApi.OData.Reader/Operation/EdmOperationOperationHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ protected override void SetParameters(OpenApiOperation operation)
167167
/// <inheritdoc/>
168168
protected override void SetResponses(OpenApiOperation operation)
169169
{
170-
operation.Responses = Context.CreateResponses(EdmOperation, Path);
170+
operation.Responses = Context.CreateResponses(EdmOperation);
171171
base.SetResponses(operation);
172172
}
173173

src/Microsoft.OpenApi.OData.Reader/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Microsoft.OpenApi.OData.Vocabulary.Core.LinkRelKey.List = 1 -> Microsoft.OpenApi
3030
Microsoft.OpenApi.OData.Vocabulary.Core.LinkRelKey.ReadByKey = 0 -> Microsoft.OpenApi.OData.Vocabulary.Core.LinkRelKey
3131
Microsoft.OpenApi.OData.Vocabulary.Core.LinkRelKey.Update = 3 -> Microsoft.OpenApi.OData.Vocabulary.Core.LinkRelKey
3232
static Microsoft.OpenApi.OData.Common.OpenApiOperationExtensions.AddErrorResponses(this Microsoft.OpenApi.Models.OpenApiOperation operation, Microsoft.OpenApi.OData.OpenApiConvertSettings settings, bool addNoContent = false, Microsoft.OpenApi.Models.OpenApiSchema schema = null) -> void
33+
static Microsoft.OpenApi.OData.Edm.EdmModelExtensions.OperationTargetsMultiplePaths(this Microsoft.OData.Edm.IEdmModel model, Microsoft.OData.Edm.IEdmOperation operation) -> bool
3334
static Microsoft.OpenApi.OData.Edm.EdmTypeExtensions.ShouldPathParameterBeQuoted(this Microsoft.OData.Edm.IEdmType edmType, Microsoft.OpenApi.OData.OpenApiConvertSettings settings) -> bool
3435
Microsoft.OpenApi.OData.Edm.IODataPathProvider
3536
Microsoft.OpenApi.OData.Edm.IODataPathProvider.CanFilter(Microsoft.OData.Edm.IEdmElement element) -> bool

0 commit comments

Comments
 (0)