Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/auto-merge-dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2.4.0
uses: dependabot/fetch-metadata@v2.5.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"

Expand Down
15 changes: 15 additions & 0 deletions redocly.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
extends:
- recommended

rules:
operation-4xx-response: off
nullable-type-sibling: off
no-server-example.com: off
no-server-trailing-slash: off
no-unused-components: off
security-defined: off
info-license-url: off
info-license: off
no-empty-servers: off
operation-summary: off
tag-description: off
160 changes: 160 additions & 0 deletions src/Microsoft.OpenApi.OData.Reader/EdmModelOpenApiExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// ------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Validation;
using Microsoft.OpenApi.OData.Common;
using Microsoft.OpenApi.OData.Edm;
using Microsoft.OpenApi.OData.Generator;
using Microsoft.OpenApi.OData.Vocabulary.Core;

namespace Microsoft.OpenApi.OData
{
Expand Down Expand Up @@ -54,5 +57,162 @@ public static OpenApiDocument ConvertToOpenApi(this IEdmModel model, OpenApiConv
ODataContext context = new(model, settings);
return context.CreateDocument();
}

/// <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(this 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 any parameter is non-nullable and not optional
return parameters.Any(p => !p.Type.IsNullable && p is not IEdmOptionalParameter);
}

/// <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(
this 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.Count == 0)
{
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(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 (!property.IsPropertyOptional())
{
return false;
}
}

return true;
}

/// <summary>
/// Checks if an individual property is optional.
/// </summary>
/// <param name="property">The EDM property.</param>
/// <returns>True if the property is optional, false if required.</returns>
private static bool IsPropertyOptional(this 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 = action.ShouldRequestBodyBeRequired(),
Content = new Dictionary<string, OpenApiMediaType>()
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,12 @@ protected override void SetParameters(OpenApiOperation operation)
}
/// <inheritdoc/>
protected override void SetRequestBody(OpenApiOperation operation)
{
{
operation.RequestBody = new OpenApiRequestBody
{
Required = true,
Required = ComplexPropertySegment?.ComplexType?.ShouldRequestBodyBeRequired(
isUpdateOperation: false,
Context?.Model) ?? true,
Description = "New property values",
Content = new Dictionary<string, OpenApiMediaType>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ protected override void SetRequestBody(OpenApiOperation operation)
{
operation.RequestBody = new OpenApiRequestBody
{
Required = true,
Required = ComplexPropertySegment?.ComplexType?.ShouldRequestBodyBeRequired(
isUpdateOperation: true,
Context?.Model) ?? true,
Description = "New property values",
Content = new Dictionary<string, OpenApiMediaType>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,12 @@ protected override void SetBasicInfo(OpenApiOperation operation)
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?.ShouldRequestBodyBeRequired(
isUpdateOperation: false,
Context?.Model) ?? true,
Description = "New entity",
Content = GetContentDescription()
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ protected override void SetRequestBody(OpenApiOperation operation)
{
operation.RequestBody = new OpenApiRequestBody
{
Required = true,
Required = EntitySet?.EntityType?.ShouldRequestBodyBeRequired(
isUpdateOperation: true,
Context?.Model) ?? true,
Description = "New property values",
Content = GetContent()
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ protected override void SetRequestBody(OpenApiOperation operation)

operation.RequestBody = new OpenApiRequestBody
{
Required = true,
Required = NavigationProperty?.ToEntityType()?.ShouldRequestBodyBeRequired(
isUpdateOperation: false,
Context?.Model) ?? true,
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,15 @@ protected override void SetBasicInfo(OpenApiOperation operation)
/// <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()?.ShouldRequestBodyBeRequired(
isUpdateOperation: true,
Context?.Model) ?? true,
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,12 @@ protected override void SetBasicInfo(OpenApiOperation operation)

/// <inheritdoc/>
protected override void SetRequestBody(OpenApiOperation operation)
{
{
operation.RequestBody = new OpenApiRequestBody
{
Required = true,
Required = Singleton?.EntityType?.ShouldRequestBodyBeRequired(
isUpdateOperation: true,
Context?.Model) ?? true,
Description = "New property values",
Content = new Dictionary<string, OpenApiMediaType>
{
Expand Down
2 changes: 2 additions & 0 deletions src/Microsoft.OpenApi.OData.Reader/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ static Microsoft.OpenApi.OData.Edm.EdmTypeExtensions.ShouldPathParameterBeQuoted
static Microsoft.OpenApi.OData.Edm.ODataRefSegment.Instance -> Microsoft.OpenApi.OData.Edm.ODataRefSegment!
static Microsoft.OpenApi.OData.EdmModelOpenApiExtensions.ConvertToOpenApi(this Microsoft.OData.Edm.IEdmModel! model) -> Microsoft.OpenApi.OpenApiDocument!
static Microsoft.OpenApi.OData.EdmModelOpenApiExtensions.ConvertToOpenApi(this Microsoft.OData.Edm.IEdmModel! model, Microsoft.OpenApi.OData.OpenApiConvertSettings! settings) -> Microsoft.OpenApi.OpenApiDocument!
static Microsoft.OpenApi.OData.EdmModelOpenApiExtensions.ShouldRequestBodyBeRequired(this Microsoft.OData.Edm.IEdmAction! action) -> bool
static Microsoft.OpenApi.OData.EdmModelOpenApiExtensions.ShouldRequestBodyBeRequired(this Microsoft.OData.Edm.IEdmStructuredType! structuredType, bool isUpdateOperation, Microsoft.OData.Edm.IEdmModel? model = null) -> bool
virtual Microsoft.OpenApi.OData.Edm.ODataPath.Kind.get -> Microsoft.OpenApi.OData.Edm.ODataPathKind
virtual Microsoft.OpenApi.OData.Edm.ODataPathProvider.CanFilter(Microsoft.OData.Edm.IEdmElement! element) -> bool
virtual Microsoft.OpenApi.OData.Edm.ODataPathProvider.GetPaths(Microsoft.OData.Edm.IEdmModel! model, Microsoft.OpenApi.OData.OpenApiConvertSettings! settings) -> System.Collections.Generic.IEnumerable<Microsoft.OpenApi.OData.Edm.ODataPath!>!
Expand Down
Loading
Loading