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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
using Microsoft.AspNetCore.OData.Routing.Template;
using Microsoft.Extensions.Options;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using static System.StringComparison;
using System.Text;
using static ODataMetadataOptions;
using static System.StringComparison;
using Opts = Microsoft.Extensions.Options.Options;

/// <summary>
Expand Down Expand Up @@ -144,6 +146,8 @@
UpdateModelTypes( result, matched );
UpdateFunctionCollectionParameters( result, matched );
}

QuoteStringParameters( result );
}

if ( results.Count > 0 )
Expand Down Expand Up @@ -526,6 +530,118 @@
description.RelativePath = path.Replace( oldValue.ToString(), newValue.ToString(), Ordinal );
}

private static void AddStringParameterNames(
IEdmFunction function,
IDictionary<string, string> parameterMappings,
ref HashSet<string>? names )
{
foreach ( var parameter in function.Parameters )
{
if ( parameter.Type.IsString() )
{
names ??= [];
names.Add( parameterMappings[parameter.Name] );
}
}
Comment on lines +538 to +545

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.

Copilot Autofix

AI 1 day ago

In general terms, to fix this, replace the explicit in-loop if-statement filtering (if (condition) { ... }) with filtration of the enumeration using LINQ's Where before entering the loop. Specifically, for the loop at line 538:

foreach ( var parameter in function.Parameters )
{
    if ( parameter.Type.IsString() )
    {
        names ??= [];
        names.Add( parameterMappings[parameter.Name] );
    }
}

should become:

foreach ( var parameter in function.Parameters.Where(p => p.Type.IsString()) )
{
    names ??= [];
    names.Add( parameterMappings[parameter.Name] );
}

This makes it clear that we're only iterating over parameters with Type.IsString(). To implement the change, you'll need to add using System.Linq; if it's not already present. Only code shown may be modified: you may add the import at the top if and only if it's missing in the lines shown.

Suggested changeset 1
src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs
--- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs
+++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs
@@ -17,6 +17,7 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Runtime.CompilerServices;
 using System.Text;
+using System.Linq;
 using static ODataMetadataOptions;
 using static System.StringComparison;
 using Opts = Microsoft.Extensions.Options.Options;
@@ -535,13 +536,10 @@
         IDictionary<string, string> parameterMappings,
         ref HashSet<string>? names )
     {
-        foreach ( var parameter in function.Parameters )
+        foreach ( var parameter in function.Parameters.Where(p => p.Type.IsString()) )
         {
-            if ( parameter.Type.IsString() )
-            {
-                names ??= [];
-                names.Add( parameterMappings[parameter.Name] );
-            }
+            names ??= [];
+            names.Add( parameterMappings[parameter.Name] );
         }
     }
 
EOF
@@ -17,6 +17,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text;
using System.Linq;
using static ODataMetadataOptions;
using static System.StringComparison;
using Opts = Microsoft.Extensions.Options.Options;
@@ -535,13 +536,10 @@
IDictionary<string, string> parameterMappings,
ref HashSet<string>? names )
{
foreach ( var parameter in function.Parameters )
foreach ( var parameter in function.Parameters.Where(p => p.Type.IsString()) )
{
if ( parameter.Type.IsString() )
{
names ??= [];
names.Add( parameterMappings[parameter.Name] );
}
names ??= [];
names.Add( parameterMappings[parameter.Name] );
}
}

Copilot is powered by AI and may make mistakes. Always verify output.
}

private static void QuoteStringParameters( ApiDescription description )
{
if ( description.RelativePath is not string path )
{
return;
}

var action = description.ActionDescriptor;
var metadata = action.EndpointMetadata;
var names = default( HashSet<string>? );

for ( var i = metadata.Count - 1; i >= 0; i-- )
{
if ( metadata[i] is not IODataRoutingMetadata odata )
{
continue;
}

for ( var j = 0; j < odata.Template.Count; j++ )
{
switch ( odata.Template[j] )
{
case KeySegmentTemplate key when key.KeyProperties.Count > 1:
foreach ( (var name, var property) in key.KeyProperties )
{
if ( property.Type.IsString() )
{
names ??= [];
names.Add( key.KeyMappings[name] );
}
}

break;

case FunctionSegmentTemplate function when function.ParameterMappings.Count > 0:
AddStringParameterNames( function.Function, function.ParameterMappings, ref names );
break;

case FunctionImportSegmentTemplate function when function.ParameterMappings.Count > 0:
AddStringParameterNames( function.FunctionImport.Function, function.ParameterMappings, ref names );
break;
}
}

break;
}

if ( names is null )
{
return;
}

var capacity = path.Length + ( names.Count << 1 );
var template = new StringBuilder( path, capacity );
var position = 0;
var inParens = false;

while ( position < template.Length )
{
switch ( template[position++] )
{
case '(':
inParens = true;
continue;
case ')':
inParens = false;
continue;
case '{':
break;
default:
continue;
}

var start = position;

while ( position < template.Length && template[position] != '}' )
{
position++;
}

if ( inParens && position < template.Length )
{
var end = position;
var name = template.ToString( start, end - start );

if ( names.Contains( name ) )
{
template.Insert( start - 1, '\'' );
template.Insert( end + 2, '\'' );
position += 2;
}
}
}

description.RelativePath = template.ToString();
}

private sealed class ApiDescriptionComparer : IEqualityComparer<ApiDescription>
{
private readonly IEqualityComparer<string?> comparer = StringComparer.OrdinalIgnoreCase;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,12 @@ private void AssertVersion0_9( ApiDescriptionGroup group )
PrintGroup( items );
group.GroupName.Should().Be( GroupName );
items.Should().BeEquivalentTo(
new[]
{
[
new { HttpMethod = "GET", GroupName, RelativePath = "api/GetHash(Input='{input}')" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/GetSalesTaxRate(PostalCode={postalCode})" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders/{key}" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/People/{key}" },
},
],
options => options.ExcludingMissingMembers() );
}

Expand All @@ -142,16 +142,19 @@ private void AssertVersion1( ApiDescriptionGroup group )
PrintGroup( items );
group.GroupName.Should().Be( GroupName );
items.Should().BeEquivalentTo(
new[]
{
[
new { HttpMethod = "GET", GroupName, RelativePath = "api/Books" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/Books/{id}" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/GetHash(Input='{input}')" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/GetSalesTaxRate(PostalCode={postalCode})" },
new { HttpMethod = "POST", GroupName, RelativePath = "api/Orders" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders/{key}" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders/MostExpensive" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/People/{key}" },
},
new { HttpMethod = "GET", GroupName, RelativePath = "api/Records(id='{id}', source={source})" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/Records" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/Records/$count" },
],
options => options.ExcludingMissingMembers() );

AssertQueryOptionWithoutOData( items[0], "filter", "author", "published" );
Expand All @@ -165,8 +168,8 @@ private void AssertVersion2( ApiDescriptionGroup group )
PrintGroup( items );
group.GroupName.Should().Be( GroupName );
items.Should().BeEquivalentTo(
new[]
{
[
new { HttpMethod = "GET", GroupName, RelativePath = "api/GetHash(Input='{input}')" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/GetSalesTaxRate(PostalCode={postalCode})" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders" },
new { HttpMethod = "POST", GroupName, RelativePath = "api/Orders" },
Expand All @@ -179,7 +182,7 @@ private void AssertVersion2( ApiDescriptionGroup group )
new { HttpMethod = "GET", GroupName, RelativePath = "api/People/{key}" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/People/$count" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/People/NewHires(Since={since})" },
},
],
options => options.ExcludingMissingMembers() );
}

Expand All @@ -189,6 +192,7 @@ private void AssertVersion3( ApiDescriptionGroup group )
var items = group.Items.OrderBy( i => i.RelativePath ).ThenBy( i => i.HttpMethod ).ToArray();
var expected = new[]
{
new { HttpMethod = "GET", GroupName, RelativePath = "api/GetHash(Input='{input}')" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/GetSalesTaxRate(PostalCode={postalCode})" },
new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders" },
new { HttpMethod = "POST", GroupName, RelativePath = "api/Orders" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string rout
ArgumentNullException.ThrowIfNull( builder );

builder.Function( "GetSalesTaxRate" ).Returns<double>().Parameter<int>( "PostalCode" );
builder.Function( "GetHash" ).Returns<int>().Parameter<string>( "Input" );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.

namespace Asp.Versioning.Simulators.Configuration;

using Asp.Versioning.OData;
using Asp.Versioning.Simulators.Models;
using Microsoft.OData.ModelBuilder;

/// <summary>
/// Represents the model configuration for records.
/// </summary>
public class RecordModelConfiguration : IModelConfiguration
{
/// <inheritdoc />
public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix )
{
ArgumentNullException.ThrowIfNull( builder );

if ( apiVersion == ApiVersions.V1 )
{
builder.EntitySet<Record>( "Records" ).EntityType.HasKey( r => new { r.Id, r.Source } );
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,13 @@ public class FunctionsController : ODataController
[HttpGet( "api/GetSalesTaxRate(PostalCode={postalCode})" )]
[ProducesResponseType( typeof( double ), Status200OK )]
public IActionResult GetSalesTaxRate( int postalCode ) => Ok( 5.6 );

/// <summary>
/// Computes the hash of the specified text.
/// </summary>
/// <param name="input">The text to hash.</param>
/// <returns>The hash of the input string.</returns>
[HttpGet( "api/GetHash(Input={input})" )]
[ProducesResponseType( typeof( int ), Status200OK )]
public IActionResult GetHash( string input ) => Ok( input.GetHashCode() );
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.

namespace Asp.Versioning.Simulators.Models;

public class Record
{
public string Id { get; set; }

public int Source { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.

namespace Asp.Versioning.Simulators.V1;

using Asp.Versioning.Simulators.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using static Microsoft.AspNetCore.Http.StatusCodes;

/// <summary>
/// Represents a RESTful record service.
/// </summary>
[ApiVersion( 1.0 )]
public class RecordsController : ODataController
{
/// <summary>
/// Gets a single record.
/// </summary>
/// <param name="id">The record identifier.</param>
/// <param name="source">The record source identifier.</param>
/// <returns>The requested record.</returns>
/// <response code="200">The record was successfully retrieved.</response>
/// <response code="404">The record does not exist.</response>
[HttpGet( "api/Records(id={id}, source={source})" )]
[Produces( "application/json" )]
[ProducesResponseType( typeof( Record ), Status200OK )]
[ProducesResponseType( Status404NotFound )]
public IActionResult Get( string id, int source ) =>
Ok( new Record()
{
Id = id,
Source = source,
} );
}
Loading