From efb6d75a843c0482b7e78f72f5abd1645ff30d08 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 11 Nov 2026 10:44:54 -0500 Subject: [PATCH 1/5] fix: support Microsoft.OData.Edm 9 on .NET 10 Multi-target the reader for net8.0 (Microsoft.OData.Edm 8.4.4) and net10.0 (Microsoft.OData.Edm 9.0.0), since Edm 9 only supports .NET 10 and is binary incompatible with 8.x. - Replace the removed GetReturn() extension with IEdmOperation.ReturnType, available in both Edm versions. - Convert Edm 9 DateOnly values for revision dates and format DateOnly/TimeOnly example values as invariant OData literals. - Let Hidi take Edm from the reader so .NET 10 consumers do not get a package downgrade. - Run the reader tests on net8.0 and net10.0. Replace the IEdmModel Moq proxy, which cannot implement Edm 9 ReadOnlySpan members, and declare key properties in invalid test models. Fixes #937 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26ff6e74-45c7-4932-bfa8-11c1c74d16e1 --- README.md | 2 + .../Microsoft.OpenApi.Hidi.csproj | 1 - .../Edm/ODataPathProvider.cs | 134 +++++++++--------- .../Edm/RecordExpressionExtensions.cs | 6 +- .../Generator/EdmSpatialTypeVisitor.cs | 4 +- .../Generator/OpenApiResponseGenerator.cs | 35 ++--- .../Microsoft.OpenAPI.OData.Reader.csproj | 12 +- .../OData/ODataPrimitiveValue.cs | 11 +- .../Operation/EdmOperationOperationHandler.cs | 42 +++--- .../Common/EdmModelHelper.cs | 4 + .../EdmVocabularyAnnotationExtensionsTests.cs | 68 +++++---- .../Edm/ODataPathProviderTests.cs | 26 ++-- ...icrosoft.OpenAPI.OData.Reader.Tests.csproj | 2 +- .../EdmActionOperationHandlerTests.cs | 30 ++-- .../EdmFunctionOperationHandlerTests.cs | 110 +++++++------- .../Capabilities/CustomParameterTests.cs | 2 +- .../Core/PrimitiveExampleValueTests.cs | 18 +-- 17 files changed, 276 insertions(+), 231 deletions(-) diff --git a/README.md b/README.md index 5169836ef..37aa17872 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ The `GetEdmModel()` method can load a model in 3 ways: The OpenAPI.OData.reader nuget package is at: [https://www.nuget.org/packages/Microsoft.OpenApi.OData/](https://www.nuget.org/packages/Microsoft.OpenApi.OData) +The package targets .NET 8, which depends on Microsoft.OData.Edm 8.x, and .NET 10, which depends on Microsoft.OData.Edm 9.x. The two Edm major versions are binary incompatible, so applications on .NET 10 or later use Edm 9.x. + ## Hidi command line tool [Hidi](src/Microsoft.OpenApi.Hidi/readme.md) validates, transforms and slices diff --git a/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj index 9f1527bad..0a95f8216 100644 --- a/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj +++ b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj @@ -44,7 +44,6 @@ all - diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs index e2bb4d781..34390c31d 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs @@ -50,15 +50,15 @@ public class ODataPathProvider : IODataPathProvider /// The collection of built . public virtual IEnumerable GetPaths(IEdmModel model, OpenApiConvertSettings settings) { - if (model == null || model.EntityContainer == null) - { - return []; - } + if (model == null || model.EntityContainer == null) + { + return []; + } - Initialize(model); + Initialize(model); - if (_model is not null) - { + if (_model is not null) + { // entity set foreach (var entitySet in _model.EntityContainer.EntitySets()) { @@ -76,13 +76,13 @@ public virtual IEnumerable GetPaths(IEdmModel model, OpenApiConvertSe RetrieveNavigationSourcePaths(singleton, settings); } } - } + } - // bound operations - RetrieveBoundOperationPaths(settings); + // bound operations + RetrieveBoundOperationPaths(settings); - if (_model is not null) - { + if (_model is not null) + { // unbound operations foreach (IEdmOperationImport import in _model.EntityContainer.OperationImports()) { @@ -91,9 +91,9 @@ public virtual IEnumerable GetPaths(IEdmModel model, OpenApiConvertSe AppendPath(new ODataPath(new ODataOperationImportSegment(import))); } } - } + } - return MergePaths(); + return MergePaths(); } /// @@ -112,32 +112,32 @@ protected virtual void Initialize(IEdmModel model) _dollarCountPaths.Clear(); } - private IEnumerable MergePaths() - { - List allODataPaths = new(); - foreach (var item in _allNavigationSourcePaths.Values) - { - allODataPaths.AddRange(item); - } + private IEnumerable MergePaths() + { + List allODataPaths = new(); + foreach (var item in _allNavigationSourcePaths.Values) + { + allODataPaths.AddRange(item); + } - foreach (var item in _allNavigationPropertyPaths.Values) - { - allODataPaths.AddRange(item); - } + foreach (var item in _allNavigationPropertyPaths.Values) + { + allODataPaths.AddRange(item); + } - allODataPaths.AddRange(_allOperationPaths); + allODataPaths.AddRange(_allOperationPaths); - allODataPaths.Sort(); + allODataPaths.Sort(); - return allODataPaths; - } + return allODataPaths; + } private void AppendPath(ODataPath path) { Utils.CheckArgumentNull(path, nameof(path)); ODataPathKind kind = path.Kind; - switch(kind) + switch (kind) { case ODataPathKind.ComplexProperty: case ODataPathKind.TypeCast: @@ -148,14 +148,14 @@ private void AppendPath(ODataPath path) case ODataPathKind.MediaEntity: if (path.FirstSegment is ODataNavigationSourceSegment navigationSourceSegment) { - if(!_allNavigationSourcePaths.TryGetValue(navigationSourceSegment.EntityType, out var nsList)) + if (!_allNavigationSourcePaths.TryGetValue(navigationSourceSegment.EntityType, out var nsList)) { nsList = []; _allNavigationSourcePaths[navigationSourceSegment.EntityType] = nsList; } - + if (kind == ODataPathKind.DollarCount) - { + { if (_allOperationPaths.FirstOrDefault(p => DollarCountAndOperationPathsSimilar(p, path)) is not null) { // Don't add a path for $count if a similar count() function path already exists. @@ -171,7 +171,7 @@ private void AppendPath(ODataPath path) dollarPathList.Add(path); } } - + nsList.Add(path); } break; @@ -213,7 +213,7 @@ private void AppendPath(ODataPath path) bool DollarCountAndOperationPathsSimilar(ODataPath path1, ODataPath path2) { - if ((path1.Kind == ODataPathKind.DollarCount && + if ((path1.Kind == ODataPathKind.DollarCount && path2.Kind == ODataPathKind.Operation && Constants.CountSegmentIdentifier.Equals(path2.LastSegment?.Identifier, StringComparison.OrdinalIgnoreCase)) || (path2.Kind == ODataPathKind.DollarCount && path1.Kind == ODataPathKind.Operation && Constants.CountSegmentIdentifier.Equals(path1.LastSegment?.Identifier, StringComparison.OrdinalIgnoreCase))) @@ -221,7 +221,7 @@ bool DollarCountAndOperationPathsSimilar(ODataPath path1, ODataPath path2) return GetModifiedPathItemName(path1)?.Equals(GetModifiedPathItemName(path2), StringComparison.OrdinalIgnoreCase) ?? false; } - return false; + return false; } string? GetModifiedPathItemName(ODataPath path) @@ -260,7 +260,7 @@ private void RetrieveNavigationSourcePaths(IEdmNavigationSource navigationSource string targetPath = path.GetTargetPath(_model); count = _model.GetRecord(targetPath, CapabilitiesConstants.CountRestrictions) ?? _model.GetRecord(entitySet, CapabilitiesConstants.CountRestrictions); - if(count?.Countable ?? true) // ~/entitySet/$count + if (count?.Countable ?? true) // ~/entitySet/$count CreateCountPath(path, convertSettings); CreateTypeCastPaths(path, convertSettings, entityType, entitySet, true); // ~/entitySet/subType @@ -435,20 +435,20 @@ private void TraverseComplexProperty(IEdmStructuralProperty structuralProperty, /// true or false. private bool ShouldCreateComplexPropertyPaths(IEdmStructuralProperty complexProperty, string targetPath, OpenApiConvertSettings convertSettings) { - Utils.CheckArgumentNull(complexProperty, nameof(complexProperty)); + Utils.CheckArgumentNull(complexProperty, nameof(complexProperty)); Utils.CheckArgumentNull(convertSettings, nameof(convertSettings)); if (!convertSettings.RequireRestrictionAnnotationsToGenerateComplexPropertyPaths) return true; - bool isReadable = _model?.GetRecord(targetPath, CapabilitiesConstants.ReadRestrictions)?.Readable - ?? _model?.GetRecord(complexProperty, CapabilitiesConstants.ReadRestrictions)?.Readable + bool isReadable = _model?.GetRecord(targetPath, CapabilitiesConstants.ReadRestrictions)?.Readable + ?? _model?.GetRecord(complexProperty, CapabilitiesConstants.ReadRestrictions)?.Readable ?? false; - bool isUpdatable = _model?.GetRecord(targetPath, CapabilitiesConstants.UpdateRestrictions)?.Updatable - ??_model?.GetRecord(complexProperty, CapabilitiesConstants.UpdateRestrictions)?.Updatable + bool isUpdatable = _model?.GetRecord(targetPath, CapabilitiesConstants.UpdateRestrictions)?.Updatable + ?? _model?.GetRecord(complexProperty, CapabilitiesConstants.UpdateRestrictions)?.Updatable ?? false; bool isInsertable = _model?.GetRecord(targetPath, CapabilitiesConstants.InsertRestrictions)?.Insertable - ?? _model?.GetRecord(complexProperty, CapabilitiesConstants.InsertRestrictions)?.Insertable + ?? _model?.GetRecord(complexProperty, CapabilitiesConstants.InsertRestrictions)?.Insertable ?? false; return isReadable || isUpdatable || isInsertable; @@ -533,8 +533,8 @@ private void RetrieveNavigationPropertyPaths( { navSourceRestrictionType = _model.GetRecord(annotatableNavigationSource, CapabilitiesConstants.NavigationRestrictions); navPropRestrictionType = _model.GetRecord(navigationProperty, CapabilitiesConstants.NavigationRestrictions); - } - + } + NavigationPropertyRestriction? restriction = navSourceRestrictionType?.RestrictedProperties? .FirstOrDefault(r => r.NavigationProperty == currentPath.NavigationPropertyPath(navigationProperty.Name)) ?? navPropRestrictionType?.RestrictedProperties?.FirstOrDefault(); @@ -567,7 +567,7 @@ private void RetrieveNavigationPropertyPaths( { IEdmEntityType navEntityType = navigationProperty.ToEntityType(); var targetsMany = navigationProperty.TargetMultiplicity() == EdmMultiplicity.Many; - + if (targetsMany) { bool? createCountPath = null; @@ -652,11 +652,11 @@ private void RetrieveNavigationPropertyPaths( if (targetsMany) { currentPath.Pop(); - } + } } currentPath.Pop(); visitedNavigationProperties.Pop(); - } + } /// /// Create $ref paths. @@ -681,7 +681,7 @@ private void CreateCountPath(ODataPath currentPath, OpenApiConvertSettings conve Utils.CheckArgumentNull(currentPath, nameof(currentPath)); Utils.CheckArgumentNull(convertSettings, nameof(convertSettings)); - if(!convertSettings.EnableDollarCountPath) + if (!convertSettings.EnableDollarCountPath) return; var countPath = currentPath.Clone(); @@ -729,12 +729,12 @@ private void CreateTypeCastPaths(ODataPath currentPath, OpenApiConvertSettings c Utils.CheckArgumentNull(structuredType, nameof(structuredType)); Utils.CheckArgumentNull(annotable, nameof(annotable)); - if(!convertSettings.EnableODataTypeCast) + if (!convertSettings.EnableODataTypeCast) return; var annotedTypeNames = GetDerivedTypeConstraintTypeNames(annotable); - - if(!annotedTypeNames.Any() && convertSettings.RequireDerivedTypesConstraintForODataTypeCastSegments) + + if (!annotedTypeNames.Any() && convertSettings.RequireDerivedTypesConstraintForODataTypeCastSegments) return; // we don't want to generate any downcast path item if there is no type cast annotation. bool filter(IEdmStructuredType x) => @@ -750,7 +750,7 @@ bool filter(IEdmStructuredType x) => .OfType() .ToArray(); - if (targetTypes is not { Length: > 0} || _model is null) return; + if (targetTypes is not { Length: > 0 } || _model is null) return; foreach (var targetType in targetTypes) { @@ -784,11 +784,11 @@ bool filter(IEdmStructuredType x) => { RetrieveNavigationPropertyPaths(declaredNavigationProperty, null, castPath, convertSettings); } - + if (targetType is IEdmEntityType entityType) { RetrieveComplexPropertyPaths(entityType, castPath, convertSettings); - } + } } } } @@ -872,15 +872,15 @@ private void RetrieveBoundOperationPaths(OpenApiConvertSettings convertSettings) var functionPaths = _allOperationPaths.Where(x => x.LastSegment is ODataOperationSegment operationSegment && operationSegment.Operation is IEdmFunction edmFunction && edmFunction.IsComposable - && edmFunction.GetReturn()?.Type is { } retType + && edmFunction.ReturnType is { } retType && retType.Definition is IEdmEntityType); - foreach( var functionPath in functionPaths) + foreach (var functionPath in functionPaths) { if (functionPath.LastSegment is not ODataOperationSegment operationSegment || operationSegment.Operation is not IEdmFunction edmFunction || !edmFunction.IsComposable - || edmFunction.GetReturn()?.Type is not { } retType + || edmFunction.ReturnType is not { } retType || retType.Definition is not IEdmEntityType returnBindingEntityType) { continue; @@ -902,7 +902,7 @@ functionPath.LastSegment is ODataOperationSegment && int composableFuncSegIndex = functionPath .Segments .OfType() - .FirstOrDefault(x => x.Operation is IEdmFunction {IsComposable: true}) is {} firstOperationSegment ? + .FirstOrDefault(x => x.Operation is IEdmFunction { IsComposable: true }) is { } firstOperationSegment ? functionPath.Segments.IndexOf(firstOperationSegment) : -1; int currentDepth = functionPath.Count - composableFuncSegIndex - 1; @@ -911,7 +911,7 @@ functionPath.LastSegment is ODataOperationSegment && ODataPath newNavigationPath = functionPath.Clone(); newNavigationPath.Push(new ODataNavigationPropertySegment(navProperty)); AppendPath(newNavigationPath); - } + } } } } @@ -969,7 +969,7 @@ secondLastPathSegment is not ODataKeySegment && var annotatable = (lastPathSegment as ODataNavigationSourceSegment)?.NavigationSource as IEdmVocabularyAnnotatable; annotatable ??= (lastPathSegment as ODataKeySegment)?.EntityType; - + if (annotatable != null && _model is not null && !EdmModelHelper.IsOperationAllowed(_model, edmOperation, annotatable)) { // Check whether the navigation source is allowed to have an operation on the entity type @@ -977,7 +977,7 @@ secondLastPathSegment is not ODataKeySegment && if (annotatable != null && !EdmModelHelper.IsOperationAllowed(_model, edmOperation, annotatable)) { continue; - } + } } if (_model is not null) @@ -990,7 +990,7 @@ secondLastPathSegment is not ODataKeySegment && } } } - private static readonly HashSet _pathKindToSkipForNavigationProperties = new () { + private static readonly HashSet _pathKindToSkipForNavigationProperties = new() { ODataPathKind.Ref, }; private void AppendBoundOperationOnNavigationPropertyPath(IEdmOperation edmOperation, bool isCollection, IEdmEntityType bindingEntityType) @@ -1002,7 +1002,7 @@ private void AppendBoundOperationOnNavigationPropertyPath(IEdmOperation edmOpera foreach (var path in value.Where(x => !_pathKindToSkipForNavigationProperties.Contains(x.Kind))) { ODataNavigationPropertySegment npSegment = path.Segments.OfType().Last(); - + if (_model is not null && !EdmModelHelper.IsOperationAllowed(_model, edmOperation, npSegment.NavigationProperty, npSegment.NavigationProperty.ContainsTarget)) { continue; @@ -1089,7 +1089,7 @@ private void AppendBoundOperationOnDerived( else { ODataPath newPath = new ODataPath(new ODataNavigationSourceSegment(ns), new ODataKeySegment(ns.EntityType), - new ODataTypeCastSegment(bindingEntityType , _model), + new ODataTypeCastSegment(bindingEntityType, _model), new ODataOperationSegment(edmOperation, isEscapedFunction, _model)); AppendPath(newPath); } @@ -1191,10 +1191,10 @@ private void AppendBoundOperationOnOperationPath(IEdmOperation edmOperation, boo foreach (var path in paths) { - if (path.LastSegment is not ODataOperationSegment operationSegment + if (path.LastSegment is not ODataOperationSegment operationSegment || (path.Segments.Count > 1 && path.Segments[path.Segments.Count - 2] is ODataOperationSegment) || operationSegment.Operation is not IEdmFunction edmFunction || !edmFunction.IsComposable - || edmFunction.GetReturn()?.Type is not { } retType || !retType.Definition.Equals(bindingEntityType) + || edmFunction.ReturnType is not { } retType || !retType.Definition.Equals(bindingEntityType) || isCollection || _model is not null && !EdmModelHelper.IsOperationAllowed(_model, edmOperation, operationSegment.Operation, true)) { diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/RecordExpressionExtensions.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/RecordExpressionExtensions.cs index 3010a06c9..7b7a3ab05 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/RecordExpressionExtensions.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/RecordExpressionExtensions.cs @@ -82,7 +82,11 @@ internal static class RecordExpressionExtensions return (record.Properties?.FirstOrDefault(e => propertyName.Equals(e.Name, StringComparison.Ordinal)) is IEdmPropertyConstructor property && property.Value is IEdmDateConstantExpression value) ? +#if NET10_0_OR_GREATER + value.Value.ToDateTime(TimeOnly.MinValue) : +#else value.Value : +#endif null; } @@ -175,7 +179,7 @@ property.Value is IEdmEnumMemberExpression value && if (record.Properties?.FirstOrDefault(e => propertyName.Equals(e.Name, StringComparison.Ordinal)) is IEdmPropertyConstructor property && property.Value is IEdmCollectionExpression value && value.Elements != null) { - IList properties = + IList properties = value.Elements .OfType() .Select(x => x.Path) diff --git a/src/Microsoft.OpenApi.OData.Reader/Generator/EdmSpatialTypeVisitor.cs b/src/Microsoft.OpenApi.OData.Reader/Generator/EdmSpatialTypeVisitor.cs index ce2065eae..545e65e48 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Generator/EdmSpatialTypeVisitor.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Generator/EdmSpatialTypeVisitor.cs @@ -49,9 +49,9 @@ public void Visit(IEdmModel model) private void VisitOperation(IEdmOperation operation) { - if (operation.GetReturn() is {} operationReturn) + if (operation.ReturnType is { } returnType) { - this.VisitTypeReference(operationReturn.Type); + this.VisitTypeReference(returnType); } foreach (var parameter in operation.Parameters) diff --git a/src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiResponseGenerator.cs b/src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiResponseGenerator.cs index fdbf03338..f4dff87e5 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiResponseGenerator.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiResponseGenerator.cs @@ -25,11 +25,12 @@ internal static class OpenApiResponseGenerator /// The created . public static IOpenApiResponse? GetResponse(this string statusCode, OpenApiDocument document) { - return statusCode switch { + return statusCode switch + { Constants.StatusCodeDefault => new OpenApiResponseReference(Constants.Error, document), - Constants.StatusCode204 => new OpenApiResponse { Description = Constants.Success}, - Constants.StatusCode201 => new OpenApiResponse { Description = Constants.Created}, - Constants.StatusCodeClass2XX => new OpenApiResponse { Description = Constants.Success}, + Constants.StatusCode204 => new OpenApiResponse { Description = Constants.Success }, + Constants.StatusCode201 => new OpenApiResponse { Description = Constants.Created }, + Constants.StatusCodeClass2XX => new OpenApiResponse { Description = Constants.Success }, Constants.StatusCodeClass4XX => new OpenApiResponseReference(Constants.Error, document), Constants.StatusCodeClass5XX => new OpenApiResponseReference(Constants.Error, document), _ => null, @@ -49,12 +50,12 @@ public static void AddResponsesToDocument(this ODataContext context, OpenApiDocu Utils.CheckArgumentNull(context, nameof(context)); Utils.CheckArgumentNull(document, nameof(document)); - var responses = new Dictionary + var responses = new Dictionary { { "error", context.CreateErrorResponse(document) } }; - if(context.Settings.EnableDollarCountPath) + if (context.Settings.EnableDollarCountPath) { responses[Constants.DollarCountSchemaName] = CreateCountResponse(document); } @@ -71,13 +72,13 @@ public static void AddResponsesToDocument(this ODataContext context, OpenApiDocu .Where(x => !responses.ContainsKey(x.Key))) .ToDictionary(x => x.Key, x => x.Value); - if(context.HasAnyNonContainedCollections()) + if (context.HasAnyNonContainedCollections()) responses[$"String{Constants.CollectionSchemaSuffix}"] = CreateCollectionResponse("String", document); foreach (IEdmOperation operation in context.Model.SchemaElements.OfType() .Where(op => context.Model.OperationTargetsMultiplePaths(op))) { - if (context.CreateOperationResponse(operation, document) is {} response) + if (context.CreateOperationResponse(operation, document) is { } response) responses[$"{operation.Name}Response"] = response; } @@ -116,8 +117,8 @@ public static OpenApiResponses CreateResponses(this ODataContext context, IEdmOp Utils.CheckArgumentNull(operation, nameof(operation)); OpenApiResponses responses = new(); - - if (operation.IsAction() && operation.GetReturn()?.Type == null && Constants.StatusCode204.GetResponse(document) is {} x204Response) + + if (operation.IsAction() && operation.ReturnType == null && Constants.StatusCode204.GetResponse(document) is { } x204Response) { responses.Add(Constants.StatusCode204, x204Response); } @@ -128,20 +129,20 @@ public static OpenApiResponses CreateResponses(this ODataContext context, IEdmOp new OpenApiResponseReference($"{operation.Name}Response", document) ); } - else if (context.CreateOperationResponse(operation, document) is {} successResponse) + else if (context.CreateOperationResponse(operation, document) is { } successResponse) { responses.Add(context.Settings.UseSuccessStatusCodeRange ? Constants.StatusCodeClass2XX : Constants.StatusCode200, successResponse); } - if (context.Settings.ErrorResponsesAsDefault && Constants.StatusCodeDefault.GetResponse(document) is {} defaultResponse) + if (context.Settings.ErrorResponsesAsDefault && Constants.StatusCodeDefault.GetResponse(document) is { } defaultResponse) { responses.Add(Constants.StatusCodeDefault, defaultResponse); } else { - if (Constants.StatusCodeClass4XX.GetResponse(document) is {} x4Response) + if (Constants.StatusCodeClass4XX.GetResponse(document) is { } x4Response) responses.Add(Constants.StatusCodeClass4XX, x4Response); - if (Constants.StatusCodeClass5XX.GetResponse(document) is {} x5Response) + if (Constants.StatusCodeClass5XX.GetResponse(document) is { } x5Response) responses.Add(Constants.StatusCodeClass5XX, x5Response); } @@ -150,7 +151,7 @@ public static OpenApiResponses CreateResponses(this ODataContext context, IEdmOp public static OpenApiResponse? CreateOperationResponse(this ODataContext context, IEdmOperation operation, OpenApiDocument document) { - if (operation.GetReturn()?.Type is not {} returnType) + if (operation.ReturnType is not { } returnType) return null; IOpenApiSchema schema; @@ -167,7 +168,7 @@ public static OpenApiResponses CreateResponses(this ODataContext context, IEdmOp } }; - if (context.Settings.EnableODataAnnotationReferencesForResponses && + if (context.Settings.EnableODataAnnotationReferencesForResponses && (operation.IsDeltaFunction() || context.Settings.EnablePagination || context.Settings.EnableCount)) { schema = new OpenApiSchema @@ -274,7 +275,7 @@ private static OpenApiResponse CreateCollectionResponse(string typeName, OpenApi Constants.ApplicationJsonMediaType, new OpenApiMediaType { - Schema = new OpenApiSchemaReference($"{typeName}{Constants.CollectionSchemaSuffix}", document) + Schema = new OpenApiSchemaReference($"{typeName}{Constants.CollectionSchemaSuffix}", document) } } } diff --git a/src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj b/src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj index a0679c795..04b21744a 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj +++ b/src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj @@ -6,7 +6,7 @@ true Microsoft Open API OData Reader Microsoft.OpenApi.OData - net8.0 + net8.0;net10.0 Microsoft.OpenApi.OData true This package contains the codes you need to convert OData CSDL to Open API Document of Model. @@ -31,7 +31,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - @@ -40,6 +39,15 @@ + + + + + + + + + True diff --git a/src/Microsoft.OpenApi.OData.Reader/OData/ODataPrimitiveValue.cs b/src/Microsoft.OpenApi.OData.Reader/OData/ODataPrimitiveValue.cs index 24a616723..f6ba94eaa 100644 --- a/src/Microsoft.OpenApi.OData.Reader/OData/ODataPrimitiveValue.cs +++ b/src/Microsoft.OpenApi.OData.Reader/OData/ODataPrimitiveValue.cs @@ -3,6 +3,9 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // ------------------------------------------------------------ +using System; +using System.Globalization; + namespace Microsoft.OpenApi.OData { /// @@ -22,7 +25,13 @@ public ODataPrimitiveValue(object value) public override string? ToString() { - return Value.ToString(); + return Value switch + { + // Match the OData literal formats of Microsoft.OData.Edm 8 Date and TimeOfDay, which Edm 9 replaced. + DateOnly date => date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), + TimeOnly time => time.ToString("HH:mm:ss.fffffff", CultureInfo.InvariantCulture), + _ => Value.ToString(), + }; } } } diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/EdmOperationOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/EdmOperationOperationHandler.cs index 3725e282d..37b69a9b6 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Operation/EdmOperationOperationHandler.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Operation/EdmOperationOperationHandler.cs @@ -27,7 +27,7 @@ internal abstract class EdmOperationOperationHandler : OperationHandler /// The document to use to lookup references. protected EdmOperationOperationHandler(OpenApiDocument document) : base(document) { - + } private OperationRestrictionsType? _operationRestriction; @@ -85,10 +85,10 @@ protected override void SetBasicInfo(OpenApiOperation operation) operation.Summary = "Invoke " + (EdmOperation?.IsAction() ?? false ? "action " : "function ") + EdmOperation?.Name; // Description - operation.Description = (string.IsNullOrEmpty(TargetPath) ? null :Context?.Model.GetDescriptionAnnotation(TargetPath)) ?? Context?.Model.GetDescriptionAnnotation(EdmOperation); + operation.Description = (string.IsNullOrEmpty(TargetPath) ? null : Context?.Model.GetDescriptionAnnotation(TargetPath)) ?? Context?.Model.GetDescriptionAnnotation(EdmOperation); // OperationId - if (Context is {Settings.EnableOperationId: true} && Path is not null) + if (Context is { Settings.EnableOperationId: true } && Path is not null) { // When the key segment is available, // its EntityType name will be used @@ -101,7 +101,7 @@ protected override void SetBasicInfo(OpenApiOperation operation) { if (segment is ODataKeySegment keySegment) { - if (!keySegment.IsAlternateKey && segment is {EntityType: not null}) + if (!keySegment.IsAlternateKey && segment is { EntityType: not null }) { identifiers.Add(segment.EntityType.Name); continue; @@ -117,7 +117,7 @@ protected override void SetBasicInfo(OpenApiOperation operation) identifiers.Add(keySegment.Identifier); } } - else if (segment is ODataOperationSegment {Identifier: not null} opSegment) + else if (segment is ODataOperationSegment { Identifier: not null } opSegment) { if (opSegment.Operation is IEdmFunction function && Context.Model.IsOperationOverload(function)) { @@ -215,7 +215,7 @@ protected override void SetParameters(OpenApiOperation operation) } /// - protected override void SetResponses(OpenApiOperation operation) + protected override void SetResponses(OpenApiOperation operation) { if (EdmOperation is not null && Context is not null) operation.Responses = Context.CreateResponses(EdmOperation, _document); @@ -254,35 +254,35 @@ protected override void AppendCustomParameters(OpenApiOperation operation) private void AppendSystemQueryOptions(IEdmFunction function, OpenApiOperation operation) { - if (function.GetReturn()?.Type is {} functionReturnType && functionReturnType.IsCollection() && Context is not null) + if (function.ReturnType is { } functionReturnType && functionReturnType.IsCollection() && Context is not null) { operation.Parameters ??= []; // $top - if (Context.CreateTop(function, _document) is {} topParameter) + if (Context.CreateTop(function, _document) is { } topParameter) { operation.Parameters.AppendParameter(topParameter); } // $skip - if (Context.CreateSkip(function, _document) is {} skipParameter) + if (Context.CreateSkip(function, _document) is { } skipParameter) { operation.Parameters.AppendParameter(skipParameter); } // $search - if (Context.CreateSearch(function, _document) is {} searchParameter) + if (Context.CreateSearch(function, _document) is { } searchParameter) { operation.Parameters.AppendParameter(searchParameter); } // $filter - if (Context.CreateFilter(function, _document) is {} filterParameter) + if (Context.CreateFilter(function, _document) is { } filterParameter) { operation.Parameters.AppendParameter(filterParameter); } // $count - if (Context.CreateCount(function, _document) is {} countParameter) + if (Context.CreateCount(function, _document) is { } countParameter) { operation.Parameters.AppendParameter(countParameter); } @@ -290,19 +290,19 @@ private void AppendSystemQueryOptions(IEdmFunction function, OpenApiOperation op if (functionReturnType?.Definition?.AsElementType() is IEdmEntityType entityType) { // $select - if (Context.CreateSelect(function, entityType) is {} selectParameter) + if (Context.CreateSelect(function, entityType) is { } selectParameter) { operation.Parameters.AppendParameter(selectParameter); } // $orderby - if (Context.CreateOrderBy(function, entityType) is {} orderbyParameter) + if (Context.CreateOrderBy(function, entityType) is { } orderbyParameter) { operation.Parameters.AppendParameter(orderbyParameter); } // $expand - if (Context.CreateExpand(function, entityType) is {} expandParameter) + if (Context.CreateExpand(function, entityType) is { } expandParameter) { operation.Parameters.AppendParameter(expandParameter); } @@ -313,20 +313,20 @@ private void AppendSystemQueryOptions(IEdmFunction function, OpenApiOperation op /// protected override void SetCustomLinkRelType() { - if (Context is {Settings.CustomHttpMethodLinkRelMapping: not null} && EdmOperation != null) + if (Context is { Settings.CustomHttpMethodLinkRelMapping: not null } && EdmOperation != null) { LinkRelKey key = EdmOperation.IsAction() ? LinkRelKey.Action : LinkRelKey.Function; Context.Settings.CustomHttpMethodLinkRelMapping.TryGetValue(key, out var linkRelValue); - CustomLinkRel = linkRelValue; + CustomLinkRel = linkRelValue; } } - + /// protected override void SetExternalDocs(OpenApiOperation operation) { if (Context is { Settings.ShowExternalDocs: true }) { - var externalDocs = (string.IsNullOrEmpty(TargetPath), string.IsNullOrEmpty(CustomLinkRel)) switch + var externalDocs = (string.IsNullOrEmpty(TargetPath), string.IsNullOrEmpty(CustomLinkRel)) switch { (false, false) => Context.Model.GetLinkRecord(TargetPath!, CustomLinkRel!), (true, false) when EdmOperation is not null => Context.Model.GetLinkRecord(EdmOperation, CustomLinkRel!), @@ -347,14 +347,14 @@ protected override void SetExternalDocs(OpenApiOperation operation) // protected override void SetExtensions(OpenApiOperation operation) { - if (Context is { Settings.EnablePagination: true } && EdmOperation?.GetReturn()?.Type?.TypeKind() == EdmTypeKind.Collection) + if (Context is { Settings.EnablePagination: true } && EdmOperation?.ReturnType?.TypeKind() == EdmTypeKind.Collection) { var extension = new JsonObject { { "nextLinkName", "@odata.nextLink"}, { "operationName", Context.Settings.PageableOperationName} }; - + operation.Extensions ??= new Dictionary(); operation.Extensions.Add(Constants.xMsPageable, new JsonNodeExtension(extension)); } diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Common/EdmModelHelper.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Common/EdmModelHelper.cs index a7d5841c3..191fa61d9 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Common/EdmModelHelper.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Common/EdmModelHelper.cs @@ -401,7 +401,11 @@ public static string GetCsdl(IEdmModel model) }; using XmlWriter xw = XmlWriter.Create(sw, settings); +#if NET10_0_OR_GREATER + CsdlWriter.TryWriteCsdl(model, xw, out var _); +#else CsdlWriter.TryWriteCsdl(model, xw, CsdlTarget.OData, out var _); +#endif xw.Flush(); string edmx = sw.ToString(); diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/EdmVocabularyAnnotationExtensionsTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/EdmVocabularyAnnotationExtensionsTests.cs index fa1d83dca..217255767 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/EdmVocabularyAnnotationExtensionsTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/EdmVocabularyAnnotationExtensionsTests.cs @@ -12,7 +12,6 @@ using Microsoft.OData.Edm.Validation; using Microsoft.OData.Edm.Vocabularies; using Microsoft.OpenApi.OData.Vocabulary.Capabilities; -using Moq; using Xunit; namespace Microsoft.OpenApi.OData.Edm.Tests @@ -36,12 +35,12 @@ public void AnnotationCacheHandlesMixedTargetHashCollisions(bool pathFirst) IEdmVocabularyAnnotatable first = pathFirst ? path : other; IEdmVocabularyAnnotatable second = pathFirst ? other : path; - Assert.Equal(pathFirst ? "path" : "other", model.Object.GetString(first, CachedStringTerm.FullName())); - Assert.Equal(pathFirst ? "other" : "path", model.Object.GetString(second, CachedStringTerm.FullName())); + Assert.Equal(pathFirst ? "path" : "other", model.GetString(first, CachedStringTerm.FullName())); + Assert.Equal(pathFirst ? "other" : "path", model.GetString(second, CachedStringTerm.FullName())); // Revisit both keys so a stored path is compared with a nonpath in either insertion order. - Assert.Equal("other", model.Object.GetString(other, CachedStringTerm.FullName())); - Assert.Equal("path", model.Object.GetString(path, CachedStringTerm.FullName())); - model.Verify(m => m.FindDeclaredVocabularyAnnotations(It.IsAny()), Times.Exactly(2)); + Assert.Equal("other", model.GetString(other, CachedStringTerm.FullName())); + Assert.Equal("path", model.GetString(path, CachedStringTerm.FullName())); + Assert.Equal(2, model.FindDeclaredVocabularyAnnotationsCallCount); } [Fact] @@ -56,10 +55,10 @@ public void AnnotationCacheSharesEquivalentPathsUsingOrdinalEquality() new EdmVocabularyAnnotation(path, CachedStringTerm, new EdmStringConstant("original")), new EdmVocabularyAnnotation(differentPath, CachedStringTerm, new EdmStringConstant("different"))); - Assert.Equal("original", model.Object.GetString(path, CachedStringTerm.FullName())); - Assert.Equal("original", model.Object.GetString(equivalentPath, CachedStringTerm.FullName())); - Assert.Equal("different", model.Object.GetString(differentPath, CachedStringTerm.FullName())); - model.Verify(m => m.FindDeclaredVocabularyAnnotations(It.IsAny()), Times.Exactly(2)); + Assert.Equal("original", model.GetString(path, CachedStringTerm.FullName())); + Assert.Equal("original", model.GetString(equivalentPath, CachedStringTerm.FullName())); + Assert.Equal("different", model.GetString(differentPath, CachedStringTerm.FullName())); + Assert.Equal(2, model.FindDeclaredVocabularyAnnotationsCallCount); } [Fact] @@ -72,10 +71,10 @@ public void AnnotationCachePreservesNonpathValueEqualityAndHashing() new EdmVocabularyAnnotation(target, CachedStringTerm, new EdmStringConstant("original")), new EdmVocabularyAnnotation(differentTarget, CachedStringTerm, new EdmStringConstant("different"))); - Assert.Equal("original", model.Object.GetString(target, CachedStringTerm.FullName())); - Assert.Equal("original", model.Object.GetString(equivalentTarget, CachedStringTerm.FullName())); - Assert.Equal("different", model.Object.GetString(differentTarget, CachedStringTerm.FullName())); - model.Verify(m => m.FindDeclaredVocabularyAnnotations(It.IsAny()), Times.Exactly(2)); + Assert.Equal("original", model.GetString(target, CachedStringTerm.FullName())); + Assert.Equal("original", model.GetString(equivalentTarget, CachedStringTerm.FullName())); + Assert.Equal("different", model.GetString(differentTarget, CachedStringTerm.FullName())); + Assert.Equal(2, model.FindDeclaredVocabularyAnnotationsCallCount); } [Fact] @@ -87,10 +86,10 @@ public void AnnotationCachePreservesNonpathReferenceEquality() new EdmVocabularyAnnotation(target, CachedStringTerm, new EdmStringConstant("original")), new EdmVocabularyAnnotation(differentTarget, CachedStringTerm, new EdmStringConstant("different"))); - Assert.Equal("original", model.Object.GetString(target, CachedStringTerm.FullName())); - Assert.Equal("different", model.Object.GetString(differentTarget, CachedStringTerm.FullName())); - Assert.Equal("original", model.Object.GetString(target, CachedStringTerm.FullName())); - model.Verify(m => m.FindDeclaredVocabularyAnnotations(It.IsAny()), Times.Exactly(2)); + Assert.Equal("original", model.GetString(target, CachedStringTerm.FullName())); + Assert.Equal("different", model.GetString(differentTarget, CachedStringTerm.FullName())); + Assert.Equal("original", model.GetString(target, CachedStringTerm.FullName())); + Assert.Equal(2, model.FindDeclaredVocabularyAnnotationsCallCount); } [Theory] @@ -104,24 +103,33 @@ public void AnnotationCacheInvalidatesBothTargetKindsWhenModelChanges(bool pathT var firstModel = GetCacheModel(new EdmVocabularyAnnotation(target, CachedStringTerm, new EdmStringConstant("first"))); var secondModel = GetCacheModel(new EdmVocabularyAnnotation(target, CachedStringTerm, new EdmStringConstant("second"))); - Assert.Equal("first", firstModel.Object.GetString(target, CachedStringTerm.FullName())); - Assert.Equal("second", secondModel.Object.GetString(target, CachedStringTerm.FullName())); - Assert.Equal("first", firstModel.Object.GetString(target, CachedStringTerm.FullName())); - firstModel.Verify(m => m.FindDeclaredVocabularyAnnotations(It.IsAny()), Times.Exactly(2)); - secondModel.Verify(m => m.FindDeclaredVocabularyAnnotations(It.IsAny()), Times.Once()); + Assert.Equal("first", firstModel.GetString(target, CachedStringTerm.FullName())); + Assert.Equal("second", secondModel.GetString(target, CachedStringTerm.FullName())); + Assert.Equal("first", firstModel.GetString(target, CachedStringTerm.FullName())); + Assert.Equal(2, firstModel.FindDeclaredVocabularyAnnotationsCallCount); + Assert.Equal(1, secondModel.FindDeclaredVocabularyAnnotationsCallCount); } - private static Mock GetCacheModel(params IEdmVocabularyAnnotation[] annotations) + private static CountingAnnotationModel GetCacheModel(params IEdmVocabularyAnnotation[] annotations) { - var model = new Mock(); - model.Setup(m => m.DirectValueAnnotationsManager).Returns(new EdmModel().DirectValueAnnotationsManager); - model.Setup(m => m.FindDeclaredTerm(CachedStringTerm.FullName())).Returns(CachedStringTerm); - // Keep the model lookup reference-based to isolate the annotation cache's key comparison. - model.Setup(m => m.FindDeclaredVocabularyAnnotations(It.IsAny())) - .Returns((IEdmVocabularyAnnotatable target) => annotations.Where(a => ReferenceEquals(a.Target, target))); + var model = new CountingAnnotationModel(annotations); + model.AddElement(CachedStringTerm); return model; } + // Moq cannot proxy IEdmModel in Microsoft.OData.Edm 9 because it has ReadOnlySpan members. + private sealed class CountingAnnotationModel(IEdmVocabularyAnnotation[] annotations) : EdmModel + { + public int FindDeclaredVocabularyAnnotationsCallCount { get; private set; } + + // Keep the model lookup reference-based to isolate the annotation cache's key comparison. + public override IEnumerable FindDeclaredVocabularyAnnotations(IEdmVocabularyAnnotatable element) + { + FindDeclaredVocabularyAnnotationsCallCount++; + return annotations.Where(a => ReferenceEquals(a.Target, element)); + } + } + private static EdmTargetPath CreateTargetPath(string entitySetName = "Items") { var container = new EdmEntityContainer("NS", "Default"); diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs index 630af975a..7d4250be5 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs @@ -122,7 +122,7 @@ public void GetPathsForGraphBetaModelWithDerivedTypesConstraintReturnsAllPaths() Assert.NotNull(paths); Assert.Equal(15861, paths.Count()); } - + [Theory] [InlineData(true)] [InlineData(false)] @@ -182,11 +182,11 @@ public void UseCountRestrictionsAnnotationsToAppendDollarCountSegmentsToNavigati if (countable) { Assert.NotNull(testPath); - } + } else { Assert.Null(testPath); - } + } } else { @@ -222,7 +222,8 @@ public void GetPathsDoesntReturnPathsForCountWhenDisabled() // Arrange IEdmModel model = GetInheritanceModel(string.Empty); ODataPathProvider provider = new ODataPathProvider(); - var settings = new OpenApiConvertSettings { + var settings = new OpenApiConvertSettings + { EnableDollarCountPath = false, AppendBoundOperationsOnDerivedTypeCastSegments = true }; @@ -281,8 +282,8 @@ public void GetOperationPathsForModelWithDerivedTypesConstraint( Assert.NotNull(paths); Assert.Equal(expectedCount, paths.Count()); var dollarCountPathsWithCastSegment = paths.Where(x => x.Kind == ODataPathKind.DollarCount && x.Any(y => y.Kind == ODataSegmentKind.TypeCast)); - if(addAnnotation && !getNavPropModel) - Assert.Single(dollarCountPathsWithCastSegment); + if (addAnnotation && !getNavPropModel) + Assert.Single(dollarCountPathsWithCastSegment); } [Theory] [InlineData(false, false, true, true, 4)] @@ -324,10 +325,10 @@ public void GetTypeCastPathsForModelWithDerivedTypesConstraint( Assert.NotNull(paths); Assert.Equal(expectedCount, paths.Count()); var dollarCountPathsWithCastSegment = paths.Where(x => x.Kind == ODataPathKind.DollarCount && x.Any(y => y.Kind == ODataSegmentKind.TypeCast)); - if(addAnnotation || !requireConstraint) - Assert.Single(dollarCountPathsWithCastSegment); + if (addAnnotation || !requireConstraint) + Assert.Single(dollarCountPathsWithCastSegment); else - Assert.Empty(dollarCountPathsWithCastSegment); + Assert.Empty(dollarCountPathsWithCastSegment); } #if DEBUG // Super useful for debugging tests. @@ -529,6 +530,7 @@ public void GetPathsWithFalseNavigabilityInNavigationRestrictionsAnnotationWorks + @@ -568,6 +570,7 @@ public void GetPathsWithFalseIndexabilityByKeyInNavigationRestrictionsAnnotation + @@ -610,6 +613,7 @@ public void GetPathsWithReferenceableNavigationPropertyWorks() + @@ -656,6 +660,7 @@ public void GetPathsWithNonReferenceableNavigationPropertyWorks() + "; @@ -690,6 +695,7 @@ public void GetPathsWithContainedNavigationPropertyWorks() + "; @@ -787,7 +793,7 @@ public void GetPathsWithAlternateKeyParametersWorks() OpenApiConvertSettings settings = new() { EnableKeyAsSegment = true, - AddAlternateKeyPaths= true + AddAlternateKeyPaths = true }; // Act diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Microsoft.OpenAPI.OData.Reader.Tests.csproj b/test/Microsoft.OpenAPI.OData.Reader.Tests/Microsoft.OpenAPI.OData.Reader.Tests.csproj index 806a65172..871200144 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Microsoft.OpenAPI.OData.Reader.Tests.csproj +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Microsoft.OpenAPI.OData.Reader.Tests.csproj @@ -3,7 +3,7 @@ Microsoft.OpenApi.OData.Reader.Tests Microsoft.OpenApi.OData.Reader.Tests - net8.0 + net8.0;net10.0 latest true ..\..\tool\Microsoft.OpenApi.OData.snk diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/EdmActionOperationHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/EdmActionOperationHandlerTests.cs index 569b62257..00d0b0990 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/EdmActionOperationHandlerTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/EdmActionOperationHandlerTests.cs @@ -20,14 +20,16 @@ public class EdmActionOperationHandlerTests { public EdmActionOperationHandlerTests() { - _openApiDocument.AddComponent("Delegated (work or school account)", new OpenApiSecurityScheme { - Type = SecuritySchemeType.OAuth2, - }); - _openApiDocument.AddComponent("Application", new OpenApiSecurityScheme { - Type = SecuritySchemeType.OAuth2, - }); - _openApiDocument.Tags ??= new HashSet(); - _openApiDocument.Tags.Add(new OpenApiTag { Name = "me.user.Actions" }); + _openApiDocument.AddComponent("Delegated (work or school account)", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + }); + _openApiDocument.AddComponent("Application", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + }); + _openApiDocument.Tags ??= new HashSet(); + _openApiDocument.Tags.Add(new OpenApiTag { Name = "me.user.Actions" }); } private readonly OpenApiDocument _openApiDocument = new(); private EdmActionOperationHandler _operationHandler => new(_openApiDocument); @@ -286,7 +288,7 @@ public async Task OperationRestrictionsTermWorksToCreateOperationForEdmAction(bo if (enableAnnotation) { - Assert.NotNull(operation.Security); + Assert.NotNull(operation.Security); Assert.Equal(2, operation.Security.Count); string json = await operation.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0); @@ -342,13 +344,13 @@ public void CreateOperationForEdmActionWithCollectionReturnTypeContainsXMsPageab var operation = _operationHandler.CreateOperation(context, path); // Assert - if (enablePagination && action.GetReturn()?.Type?.IsCollection() == true) + if (enablePagination && action.ReturnType?.IsCollection() == true) { - Assert.True(operation.Extensions.ContainsKey(Common.Constants.xMsPageable)); + Assert.True(operation.Extensions.ContainsKey(Common.Constants.xMsPageable)); } else { - Assert.DoesNotContain(Common.Constants.xMsPageable, operation.Extensions?.Keys.Select(static x => x) ?? []); + Assert.DoesNotContain(Common.Constants.xMsPageable, operation.Extensions?.Keys.Select(static x => x) ?? []); } } @@ -390,8 +392,8 @@ public void CreateOperationForEdmActionWithCollectionReturnTypeHasResponseWithNe } else if (enablePagination) - { - Assert.True(responseProperties.ContainsKey("@odata.nextLink")); + { + Assert.True(responseProperties.ContainsKey("@odata.nextLink")); } else { diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/EdmFunctionOperationHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/EdmFunctionOperationHandlerTests.cs index 636108ffa..778d190ae 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/EdmFunctionOperationHandlerTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/EdmFunctionOperationHandlerTests.cs @@ -20,14 +20,16 @@ public class EdmFunctionOperationHandlerTests { public EdmFunctionOperationHandlerTests() { - _openApiDocument.AddComponent("Delegated (work or school account)", new OpenApiSecurityScheme { - Type = SecuritySchemeType.OAuth2, - }); - _openApiDocument.AddComponent("Application", new OpenApiSecurityScheme { - Type = SecuritySchemeType.OAuth2, - }); - _openApiDocument.Tags ??= new HashSet(); - _openApiDocument.Tags.Add(new OpenApiTag { Name = "me.user.Functions" }); + _openApiDocument.AddComponent("Delegated (work or school account)", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + }); + _openApiDocument.AddComponent("Application", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + }); + _openApiDocument.Tags ??= new HashSet(); + _openApiDocument.Tags.Add(new OpenApiTag { Name = "me.user.Functions" }); } private readonly OpenApiDocument _openApiDocument = new(); @@ -36,51 +38,51 @@ public EdmFunctionOperationHandlerTests() [Fact] public void SetsDeprecationInformation() { - // Arrange - IEdmModel model = EdmModelHelper.TripServiceModel; - ODataContext context = new(model); - IEdmEntitySet people = model.EntityContainer.FindEntitySet("People"); - Assert.NotNull(people); - - IEdmFunction function = model.SchemaElements.OfType().First(f => f.Name == "GetFriendsTrips"); - Assert.NotNull(function); - - ODataPath path = new(new ODataNavigationSourceSegment(people), new ODataKeySegment(people.EntityType), new ODataOperationSegment(function)); - - // Act - var operation = _operationHandler.CreateOperation(context, path); - - // Assert - Assert.NotNull(operation); - Assert.True(operation.Deprecated); - Assert.NotEmpty(operation.Extensions); - var extension = operation.Extensions["x-ms-deprecation"]; - Assert.NotNull(extension); + // Arrange + IEdmModel model = EdmModelHelper.TripServiceModel; + ODataContext context = new(model); + IEdmEntitySet people = model.EntityContainer.FindEntitySet("People"); + Assert.NotNull(people); + + IEdmFunction function = model.SchemaElements.OfType().First(f => f.Name == "GetFriendsTrips"); + Assert.NotNull(function); + + ODataPath path = new(new ODataNavigationSourceSegment(people), new ODataKeySegment(people.EntityType), new ODataOperationSegment(function)); + + // Act + var operation = _operationHandler.CreateOperation(context, path); + + // Assert + Assert.NotNull(operation); + Assert.True(operation.Deprecated); + Assert.NotEmpty(operation.Extensions); + var extension = operation.Extensions["x-ms-deprecation"]; + Assert.NotNull(extension); } [Fact] public void DoesntSetDeprecationInformation() { - // Arrange - IEdmModel model = EdmModelHelper.TripServiceModel; - ODataContext context = new(model); - IEdmEntitySet people = model.EntityContainer.FindEntitySet("People"); - Assert.NotNull(people); - - IEdmFunction function = model.SchemaElements.OfType().First(f => f.Name == "GetFavoriteAirline"); - Assert.NotNull(function); - - ODataPath path = new(new ODataNavigationSourceSegment(people), new ODataKeySegment(people.EntityType), new ODataOperationSegment(function)); - - // Act - var operation = _operationHandler.CreateOperation(context, path); - - // Assert - Assert.NotNull(operation); - Assert.True(operation.Deprecated); - Assert.NotEmpty(operation.Extensions); - var extension = operation.Extensions["x-ms-deprecation"]; - Assert.NotNull(extension); + // Arrange + IEdmModel model = EdmModelHelper.TripServiceModel; + ODataContext context = new(model); + IEdmEntitySet people = model.EntityContainer.FindEntitySet("People"); + Assert.NotNull(people); + + IEdmFunction function = model.SchemaElements.OfType().First(f => f.Name == "GetFavoriteAirline"); + Assert.NotNull(function); + + ODataPath path = new(new ODataNavigationSourceSegment(people), new ODataKeySegment(people.EntityType), new ODataOperationSegment(function)); + + // Act + var operation = _operationHandler.CreateOperation(context, path); + + // Assert + Assert.NotNull(operation); + Assert.True(operation.Deprecated); + Assert.NotEmpty(operation.Extensions); + var extension = operation.Extensions["x-ms-deprecation"]; + Assert.NotNull(extension); } #endregion @@ -245,7 +247,7 @@ public void CreateOperationForEdmFunctionWithTypeCastReturnsCorrectOperationId(b new ODataKeySegment(customer), new ODataTypeCastSegment(vipCustomer, model), new ODataOperationSegment(function)); - + // Act var operation = _operationHandler.CreateOperation(context, path); @@ -492,7 +494,7 @@ public async Task OperationRestrictionsTermWorksToCreateOperationForEdmFunction( if (enableAnnotation) { - Assert.NotNull(operation.Security); + Assert.NotNull(operation.Security); Assert.Equal(2, operation.Security.Count); string json = await operation.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0); @@ -546,13 +548,13 @@ public void CreateOperationForEdmFunctionWithCollectionReturnTypeContainsXMsPage var operation = _operationHandler.CreateOperation(context, path); // Assert - if (enablePagination && function.GetReturn()?.Type?.IsCollection() == true) + if (enablePagination && function.ReturnType?.IsCollection() == true) { - Assert.True(operation.Extensions.ContainsKey(Common.Constants.xMsPageable)); + Assert.True(operation.Extensions.ContainsKey(Common.Constants.xMsPageable)); } else { - Assert.DoesNotContain(Common.Constants.xMsPageable, operation.Extensions?.Keys.Select(static x => x) ?? []); + Assert.DoesNotContain(Common.Constants.xMsPageable, operation.Extensions?.Keys.Select(static x => x) ?? []); } } @@ -580,7 +582,7 @@ public void CreateOperationForFunctionWithDateTimeParametersReturnsCorrectPathIt // Act OpenApiOperation operation = _operationHandler.CreateOperation(context, path); string pathItemName = path.GetPathItemName(); - + // Assert Assert.NotNull(operation); Assert.Equal("/reports/microsoft.graph.getPrinterUsageSummary(printerId={printerId},periodStart={periodStart},periodEnd={periodEnd})", pathItemName); diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Vocabulary/Capabilities/CustomParameterTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Vocabulary/Capabilities/CustomParameterTests.cs index 4e0b12e8a..4e7d1f1f1 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Vocabulary/Capabilities/CustomParameterTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Vocabulary/Capabilities/CustomParameterTests.cs @@ -103,7 +103,7 @@ private static void VerifyCustomParameter(CustomParameter parameter) value = parameter.ExampleValues[1]; Assert.Null(value.Description); Assert.NotNull(value.Value); - Assert.Equal(new TimeOnly(3, 4, 5, 6).ToString("HH:mm:ss.fffffff"), value.Value.Value.ToString()); + Assert.Equal(new TimeOnly(3, 4, 5, 6).ToString("HH:mm:ss.fffffff"), value.Value.ToString()); } private IEdmModel GetEdmModel(string annotation) diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Vocabulary/Core/PrimitiveExampleValueTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Vocabulary/Core/PrimitiveExampleValueTests.cs index 00f92cc3c..2e2f36872 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Vocabulary/Core/PrimitiveExampleValueTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Vocabulary/Core/PrimitiveExampleValueTests.cs @@ -108,15 +108,15 @@ public void PrimitiveExamplevalueInitializeWorksForPrimitiveData(string data, ob Assert.NotNull(value.Value); switch (except) { - case DateOnly dateOnly: - Assert.Equal(dateOnly.ToString("yyyy-MM-dd"), value.Value.Value.ToString()); - break; - case TimeOnly timeOnly: - Assert.Equal(timeOnly.ToString("HH:mm:ss.fffffff"), value.Value.Value.ToString()); - break; - default: - Assert.Equal(except, value.Value.Value); - break; + case DateOnly dateOnly: + Assert.Equal(dateOnly.ToString("yyyy-MM-dd"), value.Value.ToString()); + break; + case TimeOnly timeOnly: + Assert.Equal(timeOnly.ToString("HH:mm:ss.fffffff"), value.Value.ToString()); + break; + default: + Assert.Equal(except, value.Value.Value); + break; } } From bc4842339c77d23c74e643ce1db59be9f77b7ab6 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 11 Nov 2026 11:01:16 -0500 Subject: [PATCH 2/5] fix!: drop support for .NET versions below 10 Target net10.0 only and depend solely on Microsoft.OData.Edm 9, removing the net8.0 multi-targeting and conditional compilation. .NET 8 is out of support for this release line. BREAKING CHANGE: Microsoft.OpenApi.OData and Hidi now target .NET 10 only and require Microsoft.OData.Edm 9.x. Consumers on earlier runtimes or Edm 8.x must stay on the previous major version. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26ff6e74-45c7-4932-bfa8-11c1c74d16e1 --- .azure-pipelines/ci-build.yml | 5 --- .azure-pipelines/hidi-release.yml | 6 +-- .github/workflows/ci-cd.yml | 2 - .github/workflows/codeql-analysis.yml | 1 - .github/workflows/sonarcloud.yml | 3 +- Dockerfile | 2 +- README.md | 5 ++- install-tool.ps1 | 2 +- scripts/verify-hidi-package-assembly.ps1 | 4 +- .../Formatters/PowerShellFormatter.cs | 8 ++-- src/Microsoft.OpenApi.Hidi/Logger.cs | 2 +- .../Microsoft.OpenApi.Hidi.csproj | 2 +- src/Microsoft.OpenApi.Hidi/OpenApiService.cs | 42 +++++++++++++------ .../OpenApiSpecVersionHelper.cs | 2 +- src/Microsoft.OpenApi.Hidi/readme.md | 4 +- .../Edm/RecordExpressionExtensions.cs | 4 -- .../Microsoft.OpenAPI.OData.Reader.csproj | 12 +----- src/OoasGui/OoasGui.csproj | 4 +- src/OoasUtil/OoasUtil.csproj | 2 +- .../Common/EdmModelHelper.cs | 4 -- ...icrosoft.OpenAPI.OData.Reader.Tests.csproj | 2 +- .../verify-hidi-package-assembly.Tests.ps1 | 10 ++--- 22 files changed, 59 insertions(+), 69 deletions(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 125618185..806f05ad9 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -73,11 +73,6 @@ extends: inputs: version: 6.x - - task: UseDotNet@2 - displayName: 'Use .NET 8' - inputs: - version: 8.x - - task: UseDotNet@2 displayName: 'Use .NET 10' inputs: diff --git a/.azure-pipelines/hidi-release.yml b/.azure-pipelines/hidi-release.yml index 9b67b8cb0..6548aed5b 100644 --- a/.azure-pipelines/hidi-release.yml +++ b/.azure-pipelines/hidi-release.yml @@ -63,10 +63,6 @@ extends: displayName: Use .NET 6 for ESRP inputs: version: 6.x - - task: UseDotNet@2 - displayName: Use .NET 8 - inputs: - version: 8.x - task: UseDotNet@2 displayName: Use .NET 10 inputs: @@ -142,7 +138,7 @@ extends: & .\artifacts\hidi\win-x64\Microsoft.OpenApi.Hidi.exe --help if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } New-Item -ItemType Directory -Force artifacts\hidi\signing | Out-Null - Copy-Item src\Microsoft.OpenApi.Hidi\bin\$(buildConfiguration)\net8.0\Microsoft.OpenApi.Hidi.dll artifacts\hidi\signing + Copy-Item src\Microsoft.OpenApi.Hidi\bin\$(buildConfiguration)\net10.0\Microsoft.OpenApi.Hidi.dll artifacts\hidi\signing Copy-Item artifacts\hidi\win-x64\Microsoft.OpenApi.Hidi.exe artifacts\hidi\signing displayName: Build, test and stage hidi binaries - task: EsrpCodeSigning@6 diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index f19603532..bf60e8eee 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -20,7 +20,6 @@ jobs: uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0 with: dotnet-version: | - 8.0.x 10.0.x - name: Data gatherer @@ -173,7 +172,6 @@ jobs: - uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0 with: dotnet-version: | - 8.0.x 10.0.x - name: Publish and smoke-test Windows executable shell: pwsh diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2c6ba1e12..5ff8c0806 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -45,7 +45,6 @@ jobs: uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0 with: dotnet-version: | - 8.0.x 10.0.x # Initializes the CodeQL tools for scanning. diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index baf883013..d8d3e72a3 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -44,7 +44,6 @@ jobs: uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0 with: dotnet-version: | - 8.0.x 10.0.x - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -126,7 +125,7 @@ jobs: CoverletOutputFormat: 'opencover' # https://github.com/microsoft/vstest/issues/4014#issuecomment-1307913682 shell: pwsh run: | - dotnet tool run dotnet-sonarscanner begin /k:"microsoft_OpenAPI.NET.OData" /o:"microsoft" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="test/**/coverage.net8.0.opencover.xml,artifacts/hidi/coverage/*opencover*.xml" /d:sonar.coverageReportPaths="artifacts/hidi/powershell-coverage/sonar-coverage.xml" + dotnet tool run dotnet-sonarscanner begin /k:"microsoft_OpenAPI.NET.OData" /o:"microsoft" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="test/**/coverage.net10.0.opencover.xml,artifacts/hidi/coverage/*opencover*.xml" /d:sonar.coverageReportPaths="artifacts/hidi/powershell-coverage/sonar-coverage.xml" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } dotnet workload restore if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/Dockerfile b/Dockerfile index 4a66f5985..29138b747 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ WORKDIR /app/hidi RUN --mount=type=secret,id=nuget_config,target=/app/hidi/NuGet.Config \ dotnet publish ./src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj -c Release -o /app/publish -p:GeneratePackageOnBuild=false -p:HidiPublicSignBuild=true -FROM mcr.microsoft.com/dotnet/runtime:8.0-jammy-chiseled AS runtime +FROM mcr.microsoft.com/dotnet/runtime:10.0-noble-chiseled AS runtime WORKDIR /app COPY --from=build-env /app/publish ./ diff --git a/README.md b/README.md index 37aa17872..88f9a4c4d 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ The `GetEdmModel()` method can load a model in 3 ways: The OpenAPI.OData.reader nuget package is at: [https://www.nuget.org/packages/Microsoft.OpenApi.OData/](https://www.nuget.org/packages/Microsoft.OpenApi.OData) -The package targets .NET 8, which depends on Microsoft.OData.Edm 8.x, and .NET 10, which depends on Microsoft.OData.Edm 9.x. The two Edm major versions are binary incompatible, so applications on .NET 10 or later use Edm 9.x. +The package targets .NET 10 and depends on Microsoft.OData.Edm 9.x. ## Hidi command line tool @@ -94,7 +94,8 @@ The package targets .NET 8, which depends on Microsoft.OData.Edm 8.x, and .NET 1 OpenAPI documents, converts OData CSDL, and generates API diagrams. The NuGet package remains `Microsoft.OpenApi.Hidi` and the command remains `hidi`. Hidi 3.x is maintained on `main`; Hidi 2.x is maintained on `support/v2`. -Both tracks use independent versions and require the .NET 8 runtime. +Both tracks use independent versions. Hidi 3.x requires the .NET 10 runtime; +Hidi 2.x requires the .NET 8 runtime. Building this repository, including the Hidi tests, requires the .NET 10 SDK. The main Hidi pipeline sets `hidiPublishingEnabled: 'true'` for stable `hidi-v3.*` tag releases; `hidiPreviewPublishingEnabled: 'false'` keeps main diff --git a/install-tool.ps1 b/install-tool.ps1 index 4d1a74c07..1f4c92ace 100644 --- a/install-tool.ps1 +++ b/install-tool.ps1 @@ -19,7 +19,7 @@ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $installedDlls = @(Get-ChildItem $toolDirectory -Force -Recurse -Filter Microsoft.OpenApi.Hidi.dll) if ($installedDlls.Count -ne 1) { throw 'Expected exactly one installed hidi DLL.' } -$builtDll = Join-Path $PSScriptRoot 'src\Microsoft.OpenApi.Hidi\bin\Release\net8.0\Microsoft.OpenApi.Hidi.dll' +$builtDll = Join-Path $PSScriptRoot 'src\Microsoft.OpenApi.Hidi\bin\Release\net10.0\Microsoft.OpenApi.Hidi.dll' if ((Get-FileHash $installedDlls[0].FullName).Hash -ne (Get-FileHash $builtDll).Hash) { throw 'Installed hidi does not match the locally built package.' } \ No newline at end of file diff --git a/scripts/verify-hidi-package-assembly.ps1 b/scripts/verify-hidi-package-assembly.ps1 index 53365fe78..a562f8f5e 100644 --- a/scripts/verify-hidi-package-assembly.ps1 +++ b/scripts/verify-hidi-package-assembly.ps1 @@ -31,8 +31,8 @@ $archive = $null try { $archive = [IO.Compression.ZipFile]::OpenRead((Resolve-Path -LiteralPath $PackagePath).Path) $assemblies = @($archive.Entries | Where-Object { $_.Name -ieq 'Microsoft.OpenApi.Hidi.dll' }) - if ($assemblies.Count -ne 1 -or $assemblies[0].FullName -cne 'tools/net8.0/any/Microsoft.OpenApi.Hidi.dll') { - throw 'Expected exactly one Hidi DLL at tools/net8.0/any/Microsoft.OpenApi.Hidi.dll.' + if ($assemblies.Count -ne 1 -or $assemblies[0].FullName -cne 'tools/net10.0/any/Microsoft.OpenApi.Hidi.dll') { + throw 'Expected exactly one Hidi DLL at tools/net10.0/any/Microsoft.OpenApi.Hidi.dll.' } [IO.Compression.ZipFileExtensions]::ExtractToFile($assemblies[0], $packagedAssembly) diff --git a/src/Microsoft.OpenApi.Hidi/Formatters/PowerShellFormatter.cs b/src/Microsoft.OpenApi.Hidi/Formatters/PowerShellFormatter.cs index 6d2282b32..03c141a76 100644 --- a/src/Microsoft.OpenApi.Hidi/Formatters/PowerShellFormatter.cs +++ b/src/Microsoft.OpenApi.Hidi/Formatters/PowerShellFormatter.cs @@ -67,7 +67,7 @@ public override void Visit(OpenApiOperation operation) var operationId = operation.OperationId; var operationTypeExtension = operation.Extensions?.GetExtension("x-ms-docs-operation-type"); - if (operationTypeExtension.IsEquals("function") && operation.Parameters is { Count :> 0}) + if (operationTypeExtension.IsEquals("function") && operation.Parameters is { Count: > 0 }) ResolveFunctionParameters(operation.Parameters); // Order matters. Resolve operationId. @@ -106,7 +106,7 @@ private static string ResolveByRefOperationId(string operationId) // Update $ref path operationId name // Ref key word is enclosed between lower-cased and upper-cased letters // Ex.: applications_GetRefCreatedOnBehalfOf to applications_GetCreatedOnBehalfOfByRef - return s_oDataRefRegex.Match(operationId).Success ? $"{s_oDataRefRegex.Replace(operationId, string.Empty)}ByRef" : operationId; + return s_oDataRefRegex.IsMatch(operationId) ? $"{s_oDataRefRegex.Replace(operationId, string.Empty)}ByRef" : operationId; } private static string ResolveODataCastOperationId(string operationId) @@ -175,8 +175,8 @@ private static void ResolveFunctionParameters(IList parameter private void AddAdditionalPropertiesToSchema(IOpenApiSchema schema) { - if (schema is OpenApiSchema openApiSchema - && !_schemaLoop.Contains(schema) + if (schema is OpenApiSchema openApiSchema + && !_schemaLoop.Contains(schema) && schema.Type.Equals(JsonSchemaType.Object)) { openApiSchema.AdditionalProperties = new OpenApiSchema() { Type = JsonSchemaType.Object }; diff --git a/src/Microsoft.OpenApi.Hidi/Logger.cs b/src/Microsoft.OpenApi.Hidi/Logger.cs index 2dd1a4ee3..26b4e8e91 100644 --- a/src/Microsoft.OpenApi.Hidi/Logger.cs +++ b/src/Microsoft.OpenApi.Hidi/Logger.cs @@ -5,7 +5,7 @@ namespace Microsoft.OpenApi.Hidi { - public static class Logger + internal static class Logger { public static ILoggerFactory ConfigureLogger(LogLevel logLevel) { diff --git a/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj index 35e4e968f..741a6d99f 100644 --- a/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj +++ b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 latest true true diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index a494b4f2e..f6edbc5fc 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -51,7 +51,7 @@ public static async Task TransformOpenApiDocumentAsync(HidiOptions options, ILog { #pragma warning disable CA1308 // Normalize strings to uppercase var extension = options.OpenApiFormat?.ToLowerInvariant(); - var inputExtension = !string.IsNullOrEmpty(extension) ? string.Concat(".", extension) + var inputExtension = !string.IsNullOrEmpty(extension) ? string.Concat(".", extension) : GetInputPathExtension(options.OpenApi, options.Csdl); #pragma warning restore CA1308 // Normalize strings to uppercase @@ -178,7 +178,10 @@ private static OpenApiDocument ApplyFilters(HidiOptions options, ILogger logger, stopwatch.Start(); document = OpenApiFilterService.CreateFilteredDocument(document, predicate); stopwatch.Stop(); - logger.LogTrace("{Timestamp}ms: Creating filtered OpenApi document with {Paths} paths.", stopwatch.ElapsedMilliseconds, document.Paths.Count); + if (logger.IsEnabled(LogLevel.Trace)) + { + logger.LogTrace("{Timestamp}ms: Creating filtered OpenApi document with {Paths} paths.", stopwatch.ElapsedMilliseconds, document.Paths.Count); + } } return document; @@ -215,7 +218,10 @@ private static async Task WriteOpenApiAsync(HidiOptions options, string openApiF await document.SerializeAsync(writer, openApiVersion, cancellationToken).ConfigureAwait(false); stopwatch.Stop(); - logger.LogTrace("Finished serializing in {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds); + if (logger.IsEnabled(LogLevel.Trace)) + { + logger.LogTrace("Finished serializing in {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds); + } await textWriter.FlushAsync(cancellationToken).ConfigureAwait(false); } } @@ -244,7 +250,10 @@ private static async Task WriteOpenApiAsync(HidiOptions options, string openApiF document = await ConvertCsdlToOpenApiAsync(filteredStream ?? stream, format, metadataVersion, options.SettingsConfig, cancellationToken).ConfigureAwait(false); stopwatch.Stop(); - logger.LogTrace("{Timestamp}ms: Generated OpenAPI with {Paths} paths.", stopwatch.ElapsedMilliseconds, document?.Paths.Count); + if (logger.IsEnabled(LogLevel.Trace)) + { + logger.LogTrace("{Timestamp}ms: Generated OpenAPI with {Paths} paths.", stopwatch.ElapsedMilliseconds, document?.Paths.Count); + } } } else if (!string.IsNullOrEmpty(options.OpenApi)) @@ -356,9 +365,12 @@ private static MemoryStream ApplyFilterToCsdl(Stream csdlStream, string entitySe walker.Walk(result.Document); logger.LogTrace("Finished walking through the OpenApi document. Generating a statistics report.."); - #pragma warning disable CA2254 - logger.LogInformation(statsVisitor.GetStatisticsReport()); - #pragma warning restore CA2254 + if (logger.IsEnabled(LogLevel.Information)) + { +#pragma warning disable CA2254 + logger.LogInformation(statsVisitor.GetStatisticsReport()); +#pragma warning restore CA2254 + } } } catch (TaskCanceledException) @@ -395,7 +407,10 @@ private static async Task ParseOpenApiAsync(string openApiFile, bool result = await OpenApiDocument.LoadAsync(stream, settings: settings, cancellationToken: cancellationToken).ConfigureAwait(false); - logger.LogTrace("{Timestamp}ms: Completed parsing.", stopwatch.ElapsedMilliseconds); + if (logger.IsEnabled(LogLevel.Trace)) + { + logger.LogTrace("{Timestamp}ms: Completed parsing.", stopwatch.ElapsedMilliseconds); + } LogErrors(logger, result); LogWarnings(logger, result); @@ -482,7 +497,7 @@ private static Dictionary> EnumerateJsonDocument(JsonElemen } else { - paths.Add(path, new() {method}); + paths.Add(path, new() { method }); } } else @@ -547,7 +562,10 @@ SecurityException or } } stopwatch.Stop(); - logger.LogTrace("{Timestamp}ms: Read file {Input}", stopwatch.ElapsedMilliseconds, input); + if (logger.IsEnabled(LogLevel.Trace)) + { + logger.LogTrace("{Timestamp}ms: Read file {Input}", stopwatch.ElapsedMilliseconds, input); + } } return stream; } @@ -637,7 +655,7 @@ private static string GetInputPathExtension(string? openapi = null, string? csdl return options.Output.FullName; } } - } + } } catch (TaskCanceledException) { @@ -766,7 +784,7 @@ internal static async Task PluginManifestAsync(HidiOptions options, ILogger logg options.OpenApi = apiDependency.ApiDescriptionUrl; } - var openApiFormat = options.OpenApiFormat ?? (!string.IsNullOrEmpty(options.OpenApi) + var openApiFormat = options.OpenApiFormat ?? (!string.IsNullOrEmpty(options.OpenApi) ? GetOpenApiFormat(options.OpenApi, logger) : OpenApiConstants.Yaml); // Load OpenAPI document diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiSpecVersionHelper.cs b/src/Microsoft.OpenApi.Hidi/OpenApiSpecVersionHelper.cs index b8bff3195..e6afc32d1 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiSpecVersionHelper.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiSpecVersionHelper.cs @@ -5,7 +5,7 @@ namespace Microsoft.OpenApi.Hidi { - public static class OpenApiSpecVersionHelper + internal static class OpenApiSpecVersionHelper { public static OpenApiSpecVersion TryParseOpenApiSpecVersion(string value) { diff --git a/src/Microsoft.OpenApi.Hidi/readme.md b/src/Microsoft.OpenApi.Hidi/readme.md index b42640a0d..34c074941 100644 --- a/src/Microsoft.OpenApi.Hidi/readme.md +++ b/src/Microsoft.OpenApi.Hidi/readme.md @@ -15,7 +15,7 @@ Hidi has these key capabilities that enable you to build different scenarios off Install [Microsoft.OpenApi.Hidi](https://www.nuget.org/packages/Microsoft.OpenApi.Hidi) from NuGet. Hidi 3.x is maintained on this repository's `main` branch and requires -.NET 8. Hidi 2.x is maintained separately on `support/v2`. +.NET 10. Hidi 2.x is maintained separately on `support/v2`. ### .NET CLI(Global) @@ -32,7 +32,7 @@ dotnet tool install --local Microsoft.OpenApi.Hidi --version 3.10.2 ### Build and install from this repository -Run these commands from the repository root with the .NET 10 SDK and .NET 8 runtime installed: +Run these commands from the repository root with the .NET 10 SDK installed: ```powershell dotnet pack src\Microsoft.OpenApi.Hidi\Microsoft.OpenApi.Hidi.csproj -c Release -o artifacts\hidi\nuget diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/RecordExpressionExtensions.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/RecordExpressionExtensions.cs index 7b7a3ab05..862acf890 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/RecordExpressionExtensions.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/RecordExpressionExtensions.cs @@ -82,11 +82,7 @@ internal static class RecordExpressionExtensions return (record.Properties?.FirstOrDefault(e => propertyName.Equals(e.Name, StringComparison.Ordinal)) is IEdmPropertyConstructor property && property.Value is IEdmDateConstantExpression value) ? -#if NET10_0_OR_GREATER value.Value.ToDateTime(TimeOnly.MinValue) : -#else - value.Value : -#endif null; } diff --git a/src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj b/src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj index 38b39807f..7809123aa 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj +++ b/src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj @@ -6,7 +6,7 @@ true Microsoft Open API OData Reader Microsoft.OpenApi.OData - net8.0;net10.0 + net10.0 Microsoft.OpenApi.OData true This package contains the codes you need to convert OData CSDL to Open API Document of Model. @@ -31,6 +31,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -39,15 +40,6 @@ - - - - - - - - - True diff --git a/src/OoasGui/OoasGui.csproj b/src/OoasGui/OoasGui.csproj index e483343d3..ff41f3f2a 100644 --- a/src/OoasGui/OoasGui.csproj +++ b/src/OoasGui/OoasGui.csproj @@ -1,6 +1,6 @@  - net8.0-windows + net10.0-windows WinExe false true @@ -16,7 +16,7 @@ - + diff --git a/src/OoasUtil/OoasUtil.csproj b/src/OoasUtil/OoasUtil.csproj index ee59d187d..db64c1ab8 100644 --- a/src/OoasUtil/OoasUtil.csproj +++ b/src/OoasUtil/OoasUtil.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Common/EdmModelHelper.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Common/EdmModelHelper.cs index 191fa61d9..42957ff8a 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Common/EdmModelHelper.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Common/EdmModelHelper.cs @@ -401,11 +401,7 @@ public static string GetCsdl(IEdmModel model) }; using XmlWriter xw = XmlWriter.Create(sw, settings); -#if NET10_0_OR_GREATER CsdlWriter.TryWriteCsdl(model, xw, out var _); -#else - CsdlWriter.TryWriteCsdl(model, xw, CsdlTarget.OData, out var _); -#endif xw.Flush(); string edmx = sw.ToString(); diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Microsoft.OpenAPI.OData.Reader.Tests.csproj b/test/Microsoft.OpenAPI.OData.Reader.Tests/Microsoft.OpenAPI.OData.Reader.Tests.csproj index 53349736e..c66d93e72 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Microsoft.OpenAPI.OData.Reader.Tests.csproj +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Microsoft.OpenAPI.OData.Reader.Tests.csproj @@ -3,7 +3,7 @@ Microsoft.OpenApi.OData.Reader.Tests Microsoft.OpenApi.OData.Reader.Tests - net8.0;net10.0 + net10.0 latest true ..\..\tool\Microsoft.OpenApi.OData.snk diff --git a/test/Microsoft.OpenApi.Hidi.Tests/verify-hidi-package-assembly.Tests.ps1 b/test/Microsoft.OpenApi.Hidi.Tests/verify-hidi-package-assembly.Tests.ps1 index 32cd844c9..53c343eed 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/verify-hidi-package-assembly.Tests.ps1 +++ b/test/Microsoft.OpenApi.Hidi.Tests/verify-hidi-package-assembly.Tests.ps1 @@ -6,10 +6,10 @@ BeforeDiscovery { @{ Name = 'matching signed DLL before NuGet signing' } @{ Name = 'different payload bytes'; Payload = 'unsigned-build'; ExpectedError = '*differs from the signed staging DLL*' } @{ Name = 'missing tool DLL'; Entries = @(); ExpectedError = '*exactly one Hidi DLL*' } - @{ Name = 'wrong tool location'; Entries = @('lib/net8.0/Microsoft.OpenApi.Hidi.dll'); ExpectedError = '*exactly one Hidi DLL*' } - @{ Name = 'duplicate tool DLL'; Entries = @('tools/net8.0/any/Microsoft.OpenApi.Hidi.dll', 'tools/net8.0/any/Microsoft.OpenApi.Hidi.dll'); ExpectedError = '*exactly one Hidi DLL*' } - @{ Name = 'extra tool DLL elsewhere'; Entries = @('tools/net8.0/any/Microsoft.OpenApi.Hidi.dll', 'lib/net8.0/Microsoft.OpenApi.Hidi.dll'); ExpectedError = '*exactly one Hidi DLL*' } - @{ Name = 'wrong entry casing'; Entries = @('tools/net8.0/any/microsoft.openapi.hidi.dll'); ExpectedError = '*exactly one Hidi DLL*' } + @{ Name = 'wrong tool location'; Entries = @('lib/net10.0/Microsoft.OpenApi.Hidi.dll'); ExpectedError = '*exactly one Hidi DLL*' } + @{ Name = 'duplicate tool DLL'; Entries = @('tools/net10.0/any/Microsoft.OpenApi.Hidi.dll', 'tools/net10.0/any/Microsoft.OpenApi.Hidi.dll'); ExpectedError = '*exactly one Hidi DLL*' } + @{ Name = 'extra tool DLL elsewhere'; Entries = @('tools/net10.0/any/Microsoft.OpenApi.Hidi.dll', 'lib/net10.0/Microsoft.OpenApi.Hidi.dll'); ExpectedError = '*exactly one Hidi DLL*' } + @{ Name = 'wrong entry casing'; Entries = @('tools/net10.0/any/microsoft.openapi.hidi.dll'); ExpectedError = '*exactly one Hidi DLL*' } @{ Name = 'unsigned package payload'; PackageStatus = 'NotSigned'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' } @{ Name = 'damaged package signature'; PackageStatus = 'HashMismatch'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' } @{ Name = 'unverifiable package signature'; PackageStatus = 'UnknownError'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' } @@ -46,7 +46,7 @@ Describe 'Hidi package signed-assembly provenance' { else { $archive = [IO.Compression.ZipFile]::Open($package, 'Create') try { - $entryNames = if ($testCase.ContainsKey('Entries')) { $Entries } else { @('tools/net8.0/any/Microsoft.OpenApi.Hidi.dll') } + $entryNames = if ($testCase.ContainsKey('Entries')) { $Entries } else { @('tools/net10.0/any/Microsoft.OpenApi.Hidi.dll') } foreach ($name in $entryNames) { $writer = [IO.StreamWriter]::new($archive.CreateEntry($name).Open()) try { $writer.Write($(if ($Payload) { $Payload } else { 'signed-staging' })) } From 03c40c0620258e2d02e9345f7b1f08d18c90d4c7 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 11 Nov 2026 11:05:02 -0500 Subject: [PATCH 3/5] ci: align workflows with .NET 10 only targets - Install only the .NET 10 SDK in the GitHub workflows. - Point Sonar at coverage.opencover.xml: with a single target framework, coverlet no longer suffixes the report with the TFM, so the previous coverage.net*.opencover.xml glob matched nothing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26ff6e74-45c7-4932-bfa8-11c1c74d16e1 --- .github/workflows/ci-cd.yml | 6 ++---- .github/workflows/codeql-analysis.yml | 3 +-- .github/workflows/sonarcloud.yml | 5 ++--- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index bf60e8eee..85e8c663c 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -19,8 +19,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0 with: - dotnet-version: | - 10.0.x + dotnet-version: 10.0.x - name: Data gatherer id: data_gatherer @@ -171,8 +170,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0 with: - dotnet-version: | - 10.0.x + dotnet-version: 10.0.x - name: Publish and smoke-test Windows executable shell: pwsh run: | diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5ff8c0806..5361ec360 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -44,8 +44,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0 with: - dotnet-version: | - 10.0.x + dotnet-version: 10.0.x # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index d8d3e72a3..87d227912 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -43,8 +43,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6.1.0 with: - dotnet-version: | - 10.0.x + dotnet-version: 10.0.x - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis @@ -125,7 +124,7 @@ jobs: CoverletOutputFormat: 'opencover' # https://github.com/microsoft/vstest/issues/4014#issuecomment-1307913682 shell: pwsh run: | - dotnet tool run dotnet-sonarscanner begin /k:"microsoft_OpenAPI.NET.OData" /o:"microsoft" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="test/**/coverage.net10.0.opencover.xml,artifacts/hidi/coverage/*opencover*.xml" /d:sonar.coverageReportPaths="artifacts/hidi/powershell-coverage/sonar-coverage.xml" + dotnet tool run dotnet-sonarscanner begin /k:"microsoft_OpenAPI.NET.OData" /o:"microsoft" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="test/**/coverage.opencover.xml,artifacts/hidi/coverage/*opencover*.xml" /d:sonar.coverageReportPaths="artifacts/hidi/powershell-coverage/sonar-coverage.xml" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } dotnet workload restore if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } From 2d6f072fc421d1f5648d49279a4409a213c0e81d Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 11 Nov 2026 11:43:42 -0500 Subject: [PATCH 4/5] refactor: remove obsolete internal compatibility members - Remove the unused obsolete ODataConstants.OdataCount field (superseded by CreateOdataCount). - Remove the CreateOrderBy/CreateSelect/CreateExpand overloads taking IEdmEntityType that only forwarded to the IEdmStructuredType overloads, and move their documentation to those overloads. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26ff6e74-45c7-4932-bfa8-11c1c74d16e1 --- .../Generator/OpenApiParameterGenerator.cs | 83 ++++++++----------- .../OData/ODataConstants.cs | 6 -- 2 files changed, 34 insertions(+), 55 deletions(-) diff --git a/src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiParameterGenerator.cs b/src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiParameterGenerator.cs index 879f4727a..4cb73c692 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiParameterGenerator.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiParameterGenerator.cs @@ -73,7 +73,7 @@ public static IList CreateParameters(this ODataContext contex Utils.CheckArgumentNull(function, nameof(function)); Utils.CheckArgumentNull(document, nameof(document)); - var parameters = new List(); + var parameters = new List(); int skip = function.IsBound ? 1 : 0; foreach (IEdmOperationParameter edmParameter in function.Parameters.Skip(skip)) { @@ -161,7 +161,7 @@ public static IList CreateKeyParameters(this ODataContext cont Utils.CheckArgumentNull(context, nameof(context)); Utils.CheckArgumentNull(keySegment, nameof(keySegment)); Utils.CheckArgumentNull(document, nameof(document)); - + if (keySegment.IsAlternateKey) return CreateAlternateKeyParameters(context, keySegment, document); @@ -234,11 +234,11 @@ public static IList CreateKeyParameters(this ODataContext cont private static List CreateAlternateKeyParameters(ODataContext context, ODataKeySegment keySegment, OpenApiDocument document) { Debug.Assert(keySegment.Kind == ODataSegmentKind.Key); - + var parameters = new List(); var entityType = keySegment.EntityType; - var alternateKeys = context.Model.GetAlternateKeysAnnotation(entityType); - + var alternateKeys = context.Model.GetAlternateKeysAnnotation(entityType); + foreach (var alternateKey in alternateKeys) { if (alternateKey.Count() == 1) @@ -255,7 +255,7 @@ private static List CreateAlternateKeyParameters(ODataContext Required = true } ); - } + } } else { @@ -273,7 +273,7 @@ private static List CreateAlternateKeyParameters(ODataContext Required = true } ); - } + } } } } @@ -317,7 +317,7 @@ public static List CreatePathParameters(this ODataPath path, { IDictionary mappings = parameterMappings[operationSegment]; var parameters = context.CreateParameters(function, document, mappings); - pathParameters.AddRange(parameters); + pathParameters.AddRange(parameters); } } @@ -454,7 +454,7 @@ public static void AppendParameter(this IList parameters, IOp Utils.CheckArgumentNull(target, nameof(target)); Utils.CheckArgumentNull(document, nameof(document)); - if (context.Model.GetRecord(target, CapabilitiesConstants.SearchRestrictions) is not {} search || search.IsSearchable) + if (context.Model.GetRecord(target, CapabilitiesConstants.SearchRestrictions) is not { } search || search.IsSearchable) { return new OpenApiParameterReference("search", document); } @@ -494,7 +494,7 @@ public static void AppendParameter(this IList parameters, IOp Utils.CheckArgumentNull(target, nameof(target)); Utils.CheckArgumentNull(document, nameof(document)); - if (context.Model.GetRecord(target, CapabilitiesConstants.CountRestrictions) is not {} count || count.IsCountable) + if (context.Model.GetRecord(target, CapabilitiesConstants.CountRestrictions) is not { } count || count.IsCountable) { return new OpenApiParameterReference("count", document); } @@ -535,7 +535,7 @@ public static void AppendParameter(this IList parameters, IOp Utils.CheckArgumentNull(target, nameof(target)); Utils.CheckArgumentNull(document, nameof(document)); - if (context.Model.GetRecord(target, CapabilitiesConstants.FilterRestrictions) is not {} filter || filter.IsFilterable) + if (context.Model.GetRecord(target, CapabilitiesConstants.FilterRestrictions) is not { } filter || filter.IsFilterable) { return new OpenApiParameterReference("filter", document); } @@ -598,18 +598,6 @@ public static void AppendParameter(this IList parameters, IOp return context.CreateOrderBy(navigationProperty, navigationProperty.ToEntityType()); } - /// - /// Create $orderby parameter for the . - /// - /// The OData context. - /// The Edm annotation target. - /// The Edm Entity type. - /// The created or null. - public static OpenApiParameter? CreateOrderBy(this ODataContext context, IEdmVocabularyAnnotatable target, IEdmEntityType entityType) - {// patchwork to avoid breaking changes - return context.CreateOrderBy(target, entityType as IEdmStructuredType); - } - public static OpenApiParameter? CreateOrderBy(this ODataContext context, string targetPath, IEdmStructuredType structuredType) { IEdmTargetPath target = context.Model.GetTargetPath(targetPath); @@ -619,6 +607,13 @@ public static void AppendParameter(this IList parameters, IOp return context.CreateOrderBy(target, structuredType); } + /// + /// Create $orderby parameter for the . + /// + /// The OData context. + /// The Edm annotation target. + /// The Edm structured type. + /// The created or null. public static OpenApiParameter? CreateOrderBy(this ODataContext context, IEdmVocabularyAnnotatable target, IEdmStructuredType structuredType) { Utils.CheckArgumentNull(context, nameof(context)); @@ -684,7 +679,7 @@ public static void AppendParameter(this IList parameters, IOp Utils.CheckArgumentNull(context, nameof(context)); Utils.CheckArgumentNull(targetPath, nameof(targetPath)); - if (context.Model.GetTargetPath(targetPath) is not {} target) + if (context.Model.GetTargetPath(targetPath) is not { } target) return null; return context.CreateSelect(target, entityType); @@ -714,18 +709,6 @@ public static void AppendParameter(this IList parameters, IOp return context.CreateSelect(navigationProperty, navigationProperty.ToEntityType()); } - /// - /// Create $select parameter for the . - /// - /// The OData context. - /// The Edm target. - /// The Edm entity type. - /// The created or null. - public static OpenApiParameter? CreateSelect(this ODataContext context, IEdmVocabularyAnnotatable target, IEdmEntityType entityType) - { // patchwork to avoid breaking changes - return context.CreateSelect(target, entityType as IEdmStructuredType); - } - public static OpenApiParameter? CreateSelect(this ODataContext context, string targetPath, IEdmStructuredType structuredType) { IEdmTargetPath target = context.Model.GetTargetPath(targetPath); @@ -735,6 +718,13 @@ public static void AppendParameter(this IList parameters, IOp return context.CreateSelect(target, structuredType); } + /// + /// Create $select parameter for the . + /// + /// The OData context. + /// The Edm target. + /// The Edm structured type. + /// The created or null. public static OpenApiParameter? CreateSelect(this ODataContext context, IEdmVocabularyAnnotatable target, IEdmStructuredType structuredType) { Utils.CheckArgumentNull(context, nameof(context)); @@ -820,18 +810,6 @@ public static void AppendParameter(this IList parameters, IOp return context.CreateExpand(navigationProperty, navigationProperty.ToEntityType()); } - /// - /// Create $expand parameter for the . - /// - /// The OData context. - /// The edm entity path. - /// The edm entity path. - /// The created or null. - public static OpenApiParameter? CreateExpand(this ODataContext context, IEdmVocabularyAnnotatable target, IEdmEntityType entityType) - { // patchwork to avoid breaking changes - return context.CreateExpand(target, entityType as IEdmStructuredType); - } - public static OpenApiParameter? CreateExpand(this ODataContext context, string targetPath, IEdmStructuredType structuredType) { IEdmTargetPath target = context.Model.GetTargetPath(targetPath); @@ -841,6 +819,13 @@ public static void AppendParameter(this IList parameters, IOp return context.CreateExpand(target, structuredType); } + /// + /// Create $expand parameter for the . + /// + /// The OData context. + /// The Edm annotation target. + /// The Edm structured type. + /// The created or null. public static OpenApiParameter? CreateExpand(this ODataContext context, IEdmVocabularyAnnotatable target, IEdmStructuredType structuredType) { Utils.CheckArgumentNull(context, nameof(context)); @@ -853,7 +838,7 @@ public static void AppendParameter(this IList parameters, IOp return null; } - List expandItems = [ "*" ]; + List expandItems = ["*"]; foreach (var property in structuredType.NavigationProperties()) { diff --git a/src/Microsoft.OpenApi.OData.Reader/OData/ODataConstants.cs b/src/Microsoft.OpenApi.OData.Reader/OData/ODataConstants.cs index 090f55f40..b8e46136f 100644 --- a/src/Microsoft.OpenApi.OData.Reader/OData/ODataConstants.cs +++ b/src/Microsoft.OpenApi.OData.Reader/OData/ODataConstants.cs @@ -25,12 +25,6 @@ internal static class ODataConstants /// public readonly static KeyValuePair OdataNextLink = new("@odata.nextLink", new OpenApiSchema { Type = JsonSchemaType.String | JsonSchemaType.Null }); - /// - /// @odata.count KeyValue pair - /// - [Obsolete("Use CreateOdataCount instead to specify the format of the count value.")] - public readonly static KeyValuePair OdataCount = new("@odata.count", new OpenApiSchema { Type = JsonSchemaType.Number | JsonSchemaType.Null, Format = "int64"}); - /// /// Creates an @odata.count KeyValue pair with the specified format. /// From 920e642615b9308ad527eca553a6d93947ca783c Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 11 Nov 2026 11:44:01 -0500 Subject: [PATCH 5/5] ci: remove .NET 6 install for ESRP signing The EsrpCodeSigning tasks were upgraded and no longer need the .NET 6 runtime, so drop the UseDotNet 6.x steps from the Azure pipelines. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26ff6e74-45c7-4932-bfa8-11c1c74d16e1 --- .azure-pipelines/ci-build.yml | 5 ----- .azure-pipelines/hidi-release.yml | 4 ---- 2 files changed, 9 deletions(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 806f05ad9..f4136466d 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -68,11 +68,6 @@ extends: Write-Host "##vso[task.setvariable variable=ODataVersion]$version" displayName: 'Validate OData component tag and project version' - - task: UseDotNet@2 - displayName: 'Use .NET 6' # needed for ESRP signing - inputs: - version: 6.x - - task: UseDotNet@2 displayName: 'Use .NET 10' inputs: diff --git a/.azure-pipelines/hidi-release.yml b/.azure-pipelines/hidi-release.yml index 6548aed5b..1083db386 100644 --- a/.azure-pipelines/hidi-release.yml +++ b/.azure-pipelines/hidi-release.yml @@ -59,10 +59,6 @@ extends: } Write-Host "##vso[task.setvariable variable=HidiVersion]$version" displayName: Validate Hidi component tag and project version - - task: UseDotNet@2 - displayName: Use .NET 6 for ESRP - inputs: - version: 6.x - task: UseDotNet@2 displayName: Use .NET 10 inputs: