Skip to content

Commit 28e1a3b

Browse files
committed
feat: allow optional body parameter
makes body parameter optional if all fields in the body are optional
1 parent 4f90396 commit 28e1a3b

32 files changed

+1045
-865
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// ------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
4+
// ------------------------------------------------------------
5+
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using Microsoft.OData.Edm;
9+
using Microsoft.OpenApi.OData.Edm;
10+
using Microsoft.OpenApi.OData.Vocabulary.Core;
11+
12+
namespace Microsoft.OpenApi.OData.Common
13+
{
14+
/// <summary>
15+
/// Utility class for analyzing EDM types to determine if request bodies should be required.
16+
/// </summary>
17+
internal static class RequestBodyRequirementAnalyzer
18+
{
19+
/// <summary>
20+
/// Determines if a request body should be required for an OData action.
21+
/// </summary>
22+
/// <param name="action">The EDM action.</param>
23+
/// <returns>True if the request body should be required, false otherwise.</returns>
24+
public static bool ShouldRequestBodyBeRequired(IEdmAction action)
25+
{
26+
if (action == null)
27+
{
28+
return true; // Safe default
29+
}
30+
31+
// Get non-binding parameters
32+
var parameters = action.IsBound
33+
? action.Parameters.Skip(1)
34+
: action.Parameters;
35+
36+
// If no parameters, body is already null (existing behavior handles this)
37+
if (!parameters.Any())
38+
{
39+
return true; // Won't matter since body will be null
40+
}
41+
42+
// Check if all parameters are nullable or optional
43+
return !parameters.All(p => p.Type.IsNullable || p is IEdmOptionalParameter);
44+
}
45+
46+
/// <summary>
47+
/// Determines if a request body should be required for an entity or complex type.
48+
/// </summary>
49+
/// <param name="structuredType">The EDM structured type.</param>
50+
/// <param name="isUpdateOperation">Whether this is an update operation (excludes key properties).</param>
51+
/// <param name="model">The EDM model for additional context.</param>
52+
/// <returns>True if the request body should be required, false otherwise.</returns>
53+
public static bool ShouldRequestBodyBeRequired(
54+
IEdmStructuredType structuredType,
55+
bool isUpdateOperation,
56+
IEdmModel? model = null)
57+
{
58+
if (structuredType == null)
59+
{
60+
return true; // Safe default
61+
}
62+
63+
return !AreAllPropertiesOptional(structuredType, isUpdateOperation, model);
64+
}
65+
66+
/// <summary>
67+
/// Checks if all properties in a structured type are optional.
68+
/// </summary>
69+
/// <param name="structuredType">The EDM structured type.</param>
70+
/// <param name="excludeKeyProperties">Whether to exclude key properties from analysis (for update operations).</param>
71+
/// <param name="model">The EDM model for additional context.</param>
72+
/// <returns>True if all properties are optional, false if any are required.</returns>
73+
private static bool AreAllPropertiesOptional(
74+
IEdmStructuredType structuredType,
75+
bool excludeKeyProperties,
76+
IEdmModel? model = null)
77+
{
78+
if (structuredType == null)
79+
{
80+
return false;
81+
}
82+
83+
// Collect all properties including inherited ones
84+
var allProperties = new List<IEdmProperty>();
85+
86+
// Get properties from current type and all base types
87+
IEdmStructuredType currentType = structuredType;
88+
while (currentType != null)
89+
{
90+
allProperties.AddRange(currentType.DeclaredStructuralProperties());
91+
allProperties.AddRange(currentType.DeclaredNavigationProperties());
92+
currentType = currentType.BaseType;
93+
}
94+
95+
// If no properties, consider optional (empty body)
96+
if (!allProperties.Any())
97+
{
98+
return true;
99+
}
100+
101+
// Get key property names if we need to exclude them
102+
HashSet<string>? keyNames = null;
103+
if (excludeKeyProperties && structuredType is IEdmEntityType entityType)
104+
{
105+
keyNames = new HashSet<string>(entityType.Key().Select(k => k.Name));
106+
}
107+
108+
// Check if ALL remaining properties are optional
109+
foreach (var property in allProperties)
110+
{
111+
// Skip key properties if requested
112+
if (keyNames != null && keyNames.Contains(property.Name))
113+
{
114+
continue;
115+
}
116+
117+
// Skip computed properties (read-only)
118+
if (model != null && property is IEdmStructuralProperty &&
119+
(model.GetBoolean(property, CoreConstants.Computed) ?? false))
120+
{
121+
continue;
122+
}
123+
124+
// If this property is required, the body must be required
125+
if (!IsPropertyOptional(property, model))
126+
{
127+
return false;
128+
}
129+
}
130+
131+
return true;
132+
}
133+
134+
/// <summary>
135+
/// Checks if an individual property is optional.
136+
/// </summary>
137+
/// <param name="property">The EDM property.</param>
138+
/// <param name="model">The EDM model for additional context.</param>
139+
/// <returns>True if the property is optional, false if required.</returns>
140+
private static bool IsPropertyOptional(IEdmProperty property, IEdmModel? model)
141+
{
142+
if (property == null)
143+
{
144+
return false;
145+
}
146+
147+
// Structural properties (primitive, enum, complex)
148+
if (property is IEdmStructuralProperty structuralProp)
149+
{
150+
// Has default value = optional
151+
if (!string.IsNullOrEmpty(structuralProp.DefaultValueString))
152+
{
153+
return true;
154+
}
155+
156+
// Type is nullable = optional
157+
if (structuralProp.Type.IsNullable)
158+
{
159+
return true;
160+
}
161+
162+
// Otherwise required
163+
return false;
164+
}
165+
166+
// Navigation properties
167+
if (property is IEdmNavigationProperty navProp)
168+
{
169+
// Navigation properties are optional if nullable
170+
return navProp.Type.IsNullable;
171+
}
172+
173+
// Unknown property type, treat as required (safe default)
174+
return false;
175+
}
176+
}
177+
}

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ internal static class OpenApiRequestBodyGenerator
7777
OpenApiRequestBody requestBody = new OpenApiRequestBody
7878
{
7979
Description = "Action parameters",
80-
Required = true,
80+
Required = RequestBodyRequirementAnalyzer.ShouldRequestBodyBeRequired(action),
8181
Content = new Dictionary<string, IOpenApiMediaType>()
8282
};
8383

@@ -159,5 +159,23 @@ private static OpenApiRequestBody CreateRefPutRequestBody(OpenApiDocument docume
159159
}
160160
};
161161
}
162+
163+
/// <summary>
164+
/// Determines if a request body should be required based on the schema properties.
165+
/// </summary>
166+
/// <param name="structuredType">The EDM structured type.</param>
167+
/// <param name="isUpdateOperation">Whether this is an update operation (excludes key properties).</param>
168+
/// <param name="model">The EDM model for additional context.</param>
169+
/// <returns>True if the request body should be required, false otherwise.</returns>
170+
internal static bool DetermineIfRequestBodyRequired(
171+
IEdmStructuredType structuredType,
172+
bool isUpdateOperation,
173+
IEdmModel? model = null)
174+
{
175+
return RequestBodyRequirementAnalyzer.ShouldRequestBodyBeRequired(
176+
structuredType,
177+
isUpdateOperation,
178+
model);
179+
}
162180
}
163181
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,15 @@ protected override void SetParameters(OpenApiOperation operation)
8484
}
8585
/// <inheritdoc/>
8686
protected override void SetRequestBody(OpenApiOperation operation)
87-
{
87+
{
8888
operation.RequestBody = new OpenApiRequestBody
8989
{
90-
Required = true,
90+
Required = ComplexPropertySegment?.ComplexType != null
91+
? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
92+
ComplexPropertySegment.ComplexType,
93+
isUpdateOperation: false,
94+
Context?.Model)
95+
: true,
9196
Description = "New property values",
9297
Content = new Dictionary<string, IOpenApiMediaType>
9398
{

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,12 @@ protected override void SetRequestBody(OpenApiOperation operation)
6363
{
6464
operation.RequestBody = new OpenApiRequestBody
6565
{
66-
Required = true,
66+
Required = ComplexPropertySegment?.ComplexType != null
67+
? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
68+
ComplexPropertySegment.ComplexType,
69+
isUpdateOperation: true,
70+
Context?.Model)
71+
: true,
6772
Description = "New property values",
6873
Content = new Dictionary<string, IOpenApiMediaType>
6974
{

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,15 @@ protected override void SetBasicInfo(OpenApiOperation operation)
6969
protected override void SetRequestBody(OpenApiOperation operation)
7070
{
7171
// The requestBody field contains a Request Body Object for the request body
72-
// that references the schema of the entity sets entity type in the global schemas.
72+
// that references the schema of the entity set's entity type in the global schemas.
7373
operation.RequestBody = new OpenApiRequestBody
7474
{
75-
Required = true,
75+
Required = EntitySet?.EntityType != null
76+
? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
77+
EntitySet.EntityType,
78+
isUpdateOperation: false,
79+
Context?.Model)
80+
: true,
7681
Description = "New entity",
7782
Content = GetContentDescription()
7883
};

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,12 @@ protected override void SetRequestBody(OpenApiOperation operation)
7777
{
7878
operation.RequestBody = new OpenApiRequestBody
7979
{
80-
Required = true,
80+
Required = EntitySet?.EntityType != null
81+
? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
82+
EntitySet.EntityType,
83+
isUpdateOperation: true,
84+
Context?.Model)
85+
: true,
8186
Description = "New property values",
8287
Content = GetContent()
8388
};

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,12 @@ protected override void SetRequestBody(OpenApiOperation operation)
6767

6868
operation.RequestBody = new OpenApiRequestBody
6969
{
70-
Required = true,
70+
Required = NavigationProperty?.ToEntityType() != null
71+
? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
72+
NavigationProperty.ToEntityType(),
73+
isUpdateOperation: false,
74+
Context?.Model)
75+
: true,
7176
Description = "New navigation property",
7277
Content = GetContent(schema, _insertRestriction?.RequestContentTypes)
7378
};

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,18 @@ protected override void SetBasicInfo(OpenApiOperation operation)
5858
/// <inheritdoc/>
5959
protected override void SetRequestBody(OpenApiOperation operation)
6060
{
61-
var schema = Context is { Settings.EnableDerivedTypesReferencesForRequestBody: true } ?
61+
var schema = Context is { Settings.EnableDerivedTypesReferencesForRequestBody: true } ?
6262
EdmModelHelper.GetDerivedTypesReferenceSchema(NavigationProperty.ToEntityType(), Context.Model, _document) :
6363
null;
6464

6565
operation.RequestBody = new OpenApiRequestBody
6666
{
67-
Required = true,
67+
Required = NavigationProperty?.ToEntityType() != null
68+
? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
69+
NavigationProperty.ToEntityType(),
70+
isUpdateOperation: true,
71+
Context?.Model)
72+
: true,
6873
Description = "New navigation property values",
6974
Content = GetContent(schema, _updateRestriction?.RequestContentTypes)
7075
};

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,15 @@ protected override void SetBasicInfo(OpenApiOperation operation)
6767

6868
/// <inheritdoc/>
6969
protected override void SetRequestBody(OpenApiOperation operation)
70-
{
70+
{
7171
operation.RequestBody = new OpenApiRequestBody
7272
{
73-
Required = true,
73+
Required = Singleton?.EntityType != null
74+
? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
75+
Singleton.EntityType,
76+
isUpdateOperation: true,
77+
Context?.Model)
78+
: true,
7479
Description = "New property values",
7580
Content = new Dictionary<string, IOpenApiMediaType>
7681
{

0 commit comments

Comments
 (0)