Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member

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?

{
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);
Copy link
Member

Choose a reason for hiding this comment

The 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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!allProperties.Any())
if (allProperties.Count == 0)

(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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
keyNames = new HashSet<string>(entityType.Key().Select(k => k.Name));
keyNames = new HashSet<string>(entityType.Key().Select(static k => k.Name), StringComparer.Ordinal);

}

// 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

View workflow job for this annotation

GitHub Actions / Build

Remove this unused method parameter 'model'. (https://rules.sonarsource.com/csharp/RSPEC-1172)

Check warning on line 140 in src/Microsoft.OpenApi.OData.Reader/Common/RequestBodyRequirementAnalyzer.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this unused method parameter 'model'. (https://rules.sonarsource.com/csharp/RSPEC-1172)
Comment on lines +138 to +140
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// <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)
/// <returns>True if the property is optional, false if required.</returns>
private static bool IsPropertyOptional(IEdmProperty property)

{
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
Expand Up @@ -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>()
};

Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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
Expand Up @@ -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,

Check warning on line 95 in src/Microsoft.OpenApi.OData.Reader/Operation/ComplexPropertyPostOperationHandler.cs

View workflow job for this annotation

GitHub Actions / Build

Remove the unnecessary Boolean literal(s). (https://rules.sonarsource.com/csharp/RSPEC-1125)
Comment on lines +90 to +95
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Required = ComplexPropertySegment?.ComplexType != null
? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
ComplexPropertySegment.ComplexType,
isUpdateOperation: false,
Context?.Model)
: true,
Required = ComplexPropertySegment?.ComplexType == null ||
OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
ComplexPropertySegment.ComplexType,
isUpdateOperation: false,
Context?.Model),

Description = "New property values",
Content = new Dictionary<string, IOpenApiMediaType>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@
{
operation.RequestBody = new OpenApiRequestBody
{
Required = true,
Required = ComplexPropertySegment?.ComplexType != null
? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
ComplexPropertySegment.ComplexType,
isUpdateOperation: true,
Context?.Model)
: true,

Check warning on line 71 in src/Microsoft.OpenApi.OData.Reader/Operation/ComplexPropertyUpdateOperationHandler.cs

View workflow job for this annotation

GitHub Actions / Build

Remove the unnecessary Boolean literal(s). (https://rules.sonarsource.com/csharp/RSPEC-1125)
Comment on lines +66 to +71
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Required = ComplexPropertySegment?.ComplexType != null
? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
ComplexPropertySegment.ComplexType,
isUpdateOperation: true,
Context?.Model)
: true,
Required = ComplexPropertySegment?.ComplexType == null ||
? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
ComplexPropertySegment.ComplexType,
isUpdateOperation: true,
Context?.Model),

( plus multiple other instances)

Description = "New property values",
Content = new Dictionary<string, IOpenApiMediaType>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,15 @@
protected override void SetRequestBody(OpenApiOperation operation)
{
// The requestBody field contains a Request Body Object for the request body
// that references the schema of the entity sets entity type in the global schemas.
// that references the schema of the entity set's entity type in the global schemas.
operation.RequestBody = new OpenApiRequestBody
{
Required = true,
Required = EntitySet?.EntityType != null
? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
EntitySet.EntityType,
isUpdateOperation: false,
Context?.Model)
: true,

Check warning on line 80 in src/Microsoft.OpenApi.OData.Reader/Operation/EntitySetPostOperationHandler.cs

View workflow job for this annotation

GitHub Actions / Build

Remove the unnecessary Boolean literal(s). (https://rules.sonarsource.com/csharp/RSPEC-1125)
Description = "New entity",
Content = GetContentDescription()
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@
{
operation.RequestBody = new OpenApiRequestBody
{
Required = true,
Required = EntitySet?.EntityType != null
? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
EntitySet.EntityType,
isUpdateOperation: true,
Context?.Model)
: true,

Check warning on line 85 in src/Microsoft.OpenApi.OData.Reader/Operation/EntityUpdateOperationHandler.cs

View workflow job for this annotation

GitHub Actions / Build

Remove the unnecessary Boolean literal(s). (https://rules.sonarsource.com/csharp/RSPEC-1125)
Description = "New property values",
Content = GetContent()
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,12 @@

operation.RequestBody = new OpenApiRequestBody
{
Required = true,
Required = NavigationProperty?.ToEntityType() != null
? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
NavigationProperty.ToEntityType(),
isUpdateOperation: false,
Context?.Model)
: true,

Check warning on line 75 in src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyPostOperationHandler.cs

View workflow job for this annotation

GitHub Actions / Build

Remove the unnecessary Boolean literal(s). (https://rules.sonarsource.com/csharp/RSPEC-1125)
Description = "New navigation property",
Content = GetContent(schema, _insertRestriction?.RequestContentTypes)
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,18 @@
/// <inheritdoc/>
protected override void SetRequestBody(OpenApiOperation operation)
{
var schema = Context is { Settings.EnableDerivedTypesReferencesForRequestBody: true } ?
var schema = Context is { Settings.EnableDerivedTypesReferencesForRequestBody: true } ?
EdmModelHelper.GetDerivedTypesReferenceSchema(NavigationProperty.ToEntityType(), Context.Model, _document) :
null;

operation.RequestBody = new OpenApiRequestBody
{
Required = true,
Required = NavigationProperty?.ToEntityType() != null
? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
NavigationProperty.ToEntityType(),
isUpdateOperation: true,
Context?.Model)
: true,

Check warning on line 72 in src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyUpdateOperationHandler.cs

View workflow job for this annotation

GitHub Actions / Build

Remove the unnecessary Boolean literal(s). (https://rules.sonarsource.com/csharp/RSPEC-1125)
Description = "New navigation property values",
Content = GetContent(schema, _updateRestriction?.RequestContentTypes)
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,15 @@

/// <inheritdoc/>
protected override void SetRequestBody(OpenApiOperation operation)
{
{
operation.RequestBody = new OpenApiRequestBody
{
Required = true,
Required = Singleton?.EntityType != null
? OpenApiRequestBodyGenerator.DetermineIfRequestBodyRequired(
Singleton.EntityType,
isUpdateOperation: true,
Context?.Model)
: true,

Check warning on line 78 in src/Microsoft.OpenApi.OData.Reader/Operation/SingletonPatchOperationHandler.cs

View workflow job for this annotation

GitHub Actions / Build

Remove the unnecessary Boolean literal(s). (https://rules.sonarsource.com/csharp/RSPEC-1125)

Check warning on line 78 in src/Microsoft.OpenApi.OData.Reader/Operation/SingletonPatchOperationHandler.cs

View workflow job for this annotation

GitHub Actions / Build

Remove the unnecessary Boolean literal(s). (https://rules.sonarsource.com/csharp/RSPEC-1125)
Description = "New property values",
Content = new Dictionary<string, IOpenApiMediaType>
{
Expand Down
Loading
Loading