-
Notifications
You must be signed in to change notification settings - Fork 65
feat: allow optional body parameter #773
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,177 @@ | ||||||||||||
| // ------------------------------------------------------------ | ||||||||||||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||||||||||||
| // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. | ||||||||||||
| // ------------------------------------------------------------ | ||||||||||||
|
|
||||||||||||
| using System.Collections.Generic; | ||||||||||||
| using System.Linq; | ||||||||||||
| using Microsoft.OData.Edm; | ||||||||||||
| using Microsoft.OpenApi.OData.Edm; | ||||||||||||
| using Microsoft.OpenApi.OData.Vocabulary.Core; | ||||||||||||
|
|
||||||||||||
| namespace Microsoft.OpenApi.OData.Common | ||||||||||||
| { | ||||||||||||
| /// <summary> | ||||||||||||
| /// Utility class for analyzing EDM types to determine if request bodies should be required. | ||||||||||||
| /// </summary> | ||||||||||||
| internal static class RequestBodyRequirementAnalyzer | ||||||||||||
| { | ||||||||||||
| /// <summary> | ||||||||||||
| /// Determines if a request body should be required for an OData action. | ||||||||||||
| /// </summary> | ||||||||||||
| /// <param name="action">The EDM action.</param> | ||||||||||||
| /// <returns>True if the request body should be required, false otherwise.</returns> | ||||||||||||
| public static bool ShouldRequestBodyBeRequired(IEdmAction action) | ||||||||||||
| { | ||||||||||||
| if (action == null) | ||||||||||||
| { | ||||||||||||
| return true; // Safe default | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Get non-binding parameters | ||||||||||||
| var parameters = action.IsBound | ||||||||||||
| ? action.Parameters.Skip(1) | ||||||||||||
| : action.Parameters; | ||||||||||||
|
|
||||||||||||
| // If no parameters, body is already null (existing behavior handles this) | ||||||||||||
| if (!parameters.Any()) | ||||||||||||
| { | ||||||||||||
| return true; // Won't matter since body will be null | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Check if all parameters are nullable or optional | ||||||||||||
| return !parameters.All(p => p.Type.IsNullable || p is IEdmOptionalParameter); | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perf, invert the condition and use any instead. This way you don't have to iterate through the whole collection. |
||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /// <summary> | ||||||||||||
| /// Determines if a request body should be required for an entity or complex type. | ||||||||||||
| /// </summary> | ||||||||||||
| /// <param name="structuredType">The EDM structured type.</param> | ||||||||||||
| /// <param name="isUpdateOperation">Whether this is an update operation (excludes key properties).</param> | ||||||||||||
| /// <param name="model">The EDM model for additional context.</param> | ||||||||||||
| /// <returns>True if the request body should be required, false otherwise.</returns> | ||||||||||||
| public static bool ShouldRequestBodyBeRequired( | ||||||||||||
| IEdmStructuredType structuredType, | ||||||||||||
| bool isUpdateOperation, | ||||||||||||
| IEdmModel? model = null) | ||||||||||||
| { | ||||||||||||
| if (structuredType == null) | ||||||||||||
| { | ||||||||||||
| return true; // Safe default | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| return !AreAllPropertiesOptional(structuredType, isUpdateOperation, model); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /// <summary> | ||||||||||||
| /// Checks if all properties in a structured type are optional. | ||||||||||||
| /// </summary> | ||||||||||||
| /// <param name="structuredType">The EDM structured type.</param> | ||||||||||||
| /// <param name="excludeKeyProperties">Whether to exclude key properties from analysis (for update operations).</param> | ||||||||||||
| /// <param name="model">The EDM model for additional context.</param> | ||||||||||||
| /// <returns>True if all properties are optional, false if any are required.</returns> | ||||||||||||
| private static bool AreAllPropertiesOptional( | ||||||||||||
| IEdmStructuredType structuredType, | ||||||||||||
| bool excludeKeyProperties, | ||||||||||||
| IEdmModel? model = null) | ||||||||||||
| { | ||||||||||||
| if (structuredType == null) | ||||||||||||
| { | ||||||||||||
| return false; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Collect all properties including inherited ones | ||||||||||||
| var allProperties = new List<IEdmProperty>(); | ||||||||||||
|
|
||||||||||||
| // Get properties from current type and all base types | ||||||||||||
| IEdmStructuredType currentType = structuredType; | ||||||||||||
| while (currentType != null) | ||||||||||||
| { | ||||||||||||
| allProperties.AddRange(currentType.DeclaredStructuralProperties()); | ||||||||||||
| allProperties.AddRange(currentType.DeclaredNavigationProperties()); | ||||||||||||
| currentType = currentType.BaseType; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // If no properties, consider optional (empty body) | ||||||||||||
| if (!allProperties.Any()) | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
(perf, since you already have a list) |
||||||||||||
| { | ||||||||||||
| return true; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Get key property names if we need to exclude them | ||||||||||||
| HashSet<string>? keyNames = null; | ||||||||||||
| if (excludeKeyProperties && structuredType is IEdmEntityType entityType) | ||||||||||||
| { | ||||||||||||
| keyNames = new HashSet<string>(entityType.Key().Select(k => k.Name)); | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Check if ALL remaining properties are optional | ||||||||||||
| foreach (var property in allProperties) | ||||||||||||
| { | ||||||||||||
| // Skip key properties if requested | ||||||||||||
| if (keyNames != null && keyNames.Contains(property.Name)) | ||||||||||||
| { | ||||||||||||
| continue; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Skip computed properties (read-only) | ||||||||||||
| if (model != null && property is IEdmStructuralProperty && | ||||||||||||
| (model.GetBoolean(property, CoreConstants.Computed) ?? false)) | ||||||||||||
| { | ||||||||||||
| continue; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // If this property is required, the body must be required | ||||||||||||
| if (!IsPropertyOptional(property, model)) | ||||||||||||
| { | ||||||||||||
| return false; | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| return true; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /// <summary> | ||||||||||||
| /// Checks if an individual property is optional. | ||||||||||||
| /// </summary> | ||||||||||||
| /// <param name="property">The EDM property.</param> | ||||||||||||
| /// <param name="model">The EDM model for additional context.</param> | ||||||||||||
| /// <returns>True if the property is optional, false if required.</returns> | ||||||||||||
| private static bool IsPropertyOptional(IEdmProperty property, IEdmModel? model) | ||||||||||||
|
Check warning on line 140 in src/Microsoft.OpenApi.OData.Reader/Common/RequestBodyRequirementAnalyzer.cs
|
||||||||||||
|
Comment on lines
+138
to
+140
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| { | ||||||||||||
| if (property == null) | ||||||||||||
| { | ||||||||||||
| return false; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Structural properties (primitive, enum, complex) | ||||||||||||
| if (property is IEdmStructuralProperty structuralProp) | ||||||||||||
| { | ||||||||||||
| // Has default value = optional | ||||||||||||
| if (!string.IsNullOrEmpty(structuralProp.DefaultValueString)) | ||||||||||||
| { | ||||||||||||
| return true; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Type is nullable = optional | ||||||||||||
| if (structuralProp.Type.IsNullable) | ||||||||||||
| { | ||||||||||||
| return true; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Otherwise required | ||||||||||||
| return false; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Navigation properties | ||||||||||||
| if (property is IEdmNavigationProperty navProp) | ||||||||||||
| { | ||||||||||||
| // Navigation properties are optional if nullable | ||||||||||||
| return navProp.Type.IsNullable; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Unknown property type, treat as required (safe default) | ||||||||||||
| return false; | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,7 +77,7 @@ internal static class OpenApiRequestBodyGenerator | |
| OpenApiRequestBody requestBody = new OpenApiRequestBody | ||
| { | ||
| Description = "Action parameters", | ||
| Required = true, | ||
| Required = RequestBodyRequirementAnalyzer.ShouldRequestBodyBeRequired(action), | ||
| Content = new Dictionary<string, IOpenApiMediaType>() | ||
| }; | ||
|
|
||
|
|
@@ -159,5 +159,23 @@ private static OpenApiRequestBody CreateRefPutRequestBody(OpenApiDocument docume | |
| } | ||
| }; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Determines if a request body should be required based on the schema properties. | ||
| /// </summary> | ||
| /// <param name="structuredType">The EDM structured type.</param> | ||
| /// <param name="isUpdateOperation">Whether this is an update operation (excludes key properties).</param> | ||
| /// <param name="model">The EDM model for additional context.</param> | ||
| /// <returns>True if the request body should be required, false otherwise.</returns> | ||
| internal static bool DetermineIfRequestBodyRequired( | ||
| IEdmStructuredType structuredType, | ||
| bool isUpdateOperation, | ||
| IEdmModel? model = null) | ||
| { | ||
| return RequestBodyRequirementAnalyzer.ShouldRequestBodyBeRequired( | ||
| structuredType, | ||
| isUpdateOperation, | ||
| model); | ||
| } | ||
|
Comment on lines
+163
to
+179
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove this method, callers can call the target method directly. |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -84,10 +84,15 @@ | |||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| /// <inheritdoc/> | ||||||||||||||||||||||||
| protected override void SetRequestBody(OpenApiOperation operation) | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| operation.RequestBody = new OpenApiRequestBody | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| Required = true, | ||||||||||||||||||||||||
| Required = ComplexPropertySegment?.ComplexType != null | ||||||||||||||||||||||||
| ? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired( | ||||||||||||||||||||||||
| ComplexPropertySegment.ComplexType, | ||||||||||||||||||||||||
| isUpdateOperation: false, | ||||||||||||||||||||||||
| Context?.Model) | ||||||||||||||||||||||||
| : true, | ||||||||||||||||||||||||
|
Comment on lines
+90
to
+95
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||
| Description = "New property values", | ||||||||||||||||||||||||
| Content = new Dictionary<string, IOpenApiMediaType> | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -63,7 +63,12 @@ | |||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| operation.RequestBody = new OpenApiRequestBody | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| Required = true, | ||||||||||||||||||||||||
| Required = ComplexPropertySegment?.ComplexType != null | ||||||||||||||||||||||||
| ? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired( | ||||||||||||||||||||||||
| ComplexPropertySegment.ComplexType, | ||||||||||||||||||||||||
| isUpdateOperation: true, | ||||||||||||||||||||||||
| Context?.Model) | ||||||||||||||||||||||||
| : true, | ||||||||||||||||||||||||
|
Comment on lines
+66
to
+71
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
( plus multiple other instances) |
||||||||||||||||||||||||
| Description = "New property values", | ||||||||||||||||||||||||
| Content = new Dictionary<string, IOpenApiMediaType> | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: why are those methods not extension methods?