Skip to content

Commit 318a702

Browse files
Fix mapping of API version endpoint metadata
1 parent 209d0fa commit 318a702

File tree

4 files changed

+106
-89
lines changed

4 files changed

+106
-89
lines changed

src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
</ItemGroup>
2626

2727
<ItemGroup>
28-
<PackageReference Include="Microsoft.AspNetCore.OData" Version="[8.0.8,9.0.0)" />
28+
<PackageReference Include="Microsoft.AspNetCore.OData" Version="[8.0.10,9.0.0)" />
2929
</ItemGroup>
3030

3131
<ItemGroup>

src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApplicationModelProvider.cs

+1-49
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,7 @@ public ODataApplicationModelProvider(
5757
private static int BeforeOData { get; } = ODataMultiModelApplicationModelProvider.DefaultODataOrder - 50;
5858

5959
/// <inheritdoc />
60-
public virtual void OnProvidersExecuted( ApplicationModelProviderContext context )
61-
{
62-
if ( context == null )
63-
{
64-
throw new ArgumentNullException( nameof( context ) );
65-
}
66-
67-
EnsureEndpointMetadata( context.Result );
68-
}
60+
public virtual void OnProvidersExecuted( ApplicationModelProviderContext context ) { }
6961

7062
/// <inheritdoc />
7163
public virtual void OnProvidersExecuting( ApplicationModelProviderContext context )
@@ -193,46 +185,6 @@ private static
193185
return bestController;
194186
}
195187

196-
private static void EnsureEndpointMetadata( ApplicationModel application )
197-
{
198-
var controllers = application.Controllers;
199-
200-
for ( var i = 0; i < controllers.Count; i++ )
201-
{
202-
var controller = controllers[i];
203-
204-
if ( !controller.ControllerType.IsMetadataController() &&
205-
!ODataControllerSpecification.IsSatisfiedBy( controller ) )
206-
{
207-
continue;
208-
}
209-
210-
var actions = controller.Actions;
211-
212-
for ( var j = 0; j < actions.Count; j++ )
213-
{
214-
var selectors = actions[j].Selectors;
215-
var metadata = default( ApiVersionMetadata );
216-
var endpointMetadata = selectors[0].EndpointMetadata;
217-
218-
for ( var k = 0; metadata == null && k < selectors.Count; k++ )
219-
{
220-
metadata = endpointMetadata[k] as ApiVersionMetadata;
221-
}
222-
223-
if ( metadata == null )
224-
{
225-
continue;
226-
}
227-
228-
for ( var k = 1; k < selectors.Count; k++ )
229-
{
230-
selectors[k].EndpointMetadata.Add( metadata );
231-
}
232-
}
233-
}
234-
}
235-
236188
private void ApplyMetadataControllerConventions(
237189
List<ControllerModel>? metadataControllers,
238190
SortedSet<ApiVersion>? supported,

src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs

+64
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,22 @@ static void NoConfig( IServiceCollection sc )
110110
provider.OnProvidersExecuted( context );
111111
}
112112

113+
// HACK: there are intrinsically a couple of issues here:
114+
//
115+
// 1. ASP.NET Core creates an ActionDescriptor per SelectorModel in an ActionModel
116+
// 2. OData adds a SelectorModel per EDM
117+
// 3. ApiVersionMetadata has already be computed and added to EndpointMetadata
118+
//
119+
// this only becomes a problem when there are multiple EDMs and a single action implementation
120+
// maps to more than one EDM.
121+
//
122+
// REF: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ApplicationModels/ActionAttributeRouteModel.cs
123+
// REF: https://github.com/OData/AspNetCoreOData/blob/main/src/Microsoft.AspNetCore.OData/Extensions/ActionModelExtensions.cs#L148
124+
if ( mapping.Count > 1 )
125+
{
126+
CopyApiVersionEndpointMetadata( context.Result.Controllers );
127+
}
128+
113129
versionedODataOptions.Mapping = mapping;
114130
}
115131

@@ -165,4 +181,52 @@ private static int FindAttributeRouteConvention( ODataOptions options )
165181

166182
return -1;
167183
}
184+
185+
private static void CopyApiVersionEndpointMetadata( IList<ControllerModel> controllers )
186+
{
187+
for ( var i = 0; i < controllers.Count; i++ )
188+
{
189+
var actions = controllers[i].Actions;
190+
191+
for ( var j = 0; j < actions.Count; j++ )
192+
{
193+
var selectors = actions[j].Selectors;
194+
195+
if ( selectors.Count < 2 )
196+
{
197+
continue;
198+
}
199+
200+
var metadata = selectors[0].EndpointMetadata.OfType<ApiVersionMetadata>().FirstOrDefault();
201+
202+
if ( metadata is null )
203+
{
204+
continue;
205+
}
206+
207+
for ( var k = 1; k < selectors.Count; k++ )
208+
{
209+
var endpointMetadata = selectors[k].EndpointMetadata;
210+
var found = false;
211+
212+
for ( var l = 0; l < endpointMetadata.Count; l++ )
213+
{
214+
if ( endpointMetadata[l] is not ApiVersionMetadata )
215+
{
216+
continue;
217+
}
218+
219+
endpointMetadata[l] = metadata;
220+
found = true;
221+
break;
222+
}
223+
224+
if ( !found )
225+
{
226+
endpointMetadata.Add( metadata );
227+
}
228+
}
229+
}
230+
}
231+
}
168232
}

src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/ApiExplorer/ODataApiDescriptionProviderTest.cs

+40-39
Original file line numberDiff line numberDiff line change
@@ -128,48 +128,49 @@ private void AssertVersion3( ApiDescriptionGroup group )
128128
{
129129
const string GroupName = "v3";
130130
var items = group.Items.OrderBy( i => i.RelativePath ).ThenBy( i => i.HttpMethod ).ToArray();
131+
var expected = new[]
132+
{
133+
new { HttpMethod = "GET", GroupName, RelativePath = "api/GetSalesTaxRate(PostalCode={postalCode})" },
134+
new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders" },
135+
new { HttpMethod = "POST", GroupName, RelativePath = "api/Orders" },
136+
new { HttpMethod = "DELETE", GroupName, RelativePath = "api/Orders/{key}" },
137+
new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders/{key}" },
138+
new { HttpMethod = "PATCH", GroupName, RelativePath = "api/Orders/{key}" },
139+
new { HttpMethod = "POST", GroupName, RelativePath = "api/Orders/{key}/Rate" },
140+
new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders/$count" },
141+
new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders/MostExpensive" },
142+
new { HttpMethod = "GET", GroupName, RelativePath = "api/People" },
143+
new { HttpMethod = "POST", GroupName, RelativePath = "api/People" },
144+
new { HttpMethod = "GET", GroupName, RelativePath = "api/People/{key}" },
145+
new { HttpMethod = "POST", GroupName, RelativePath = "api/People/{key}/Promote" },
146+
new { HttpMethod = "GET", GroupName, RelativePath = "api/People/$count" },
147+
new { HttpMethod = "GET", GroupName, RelativePath = "api/People/NewHires(Since={since})" },
148+
new { HttpMethod = "GET", GroupName, RelativePath = "api/Products" },
149+
new { HttpMethod = "POST", GroupName, RelativePath = "api/Products" },
150+
new { HttpMethod = "DELETE", GroupName, RelativePath = "api/Products/{key}" },
151+
new { HttpMethod = "GET", GroupName, RelativePath = "api/Products/{key}" },
152+
new { HttpMethod = "GET", GroupName, RelativePath = "api/Products/$count" },
153+
new { HttpMethod = "PATCH", GroupName, RelativePath = "api/Products/{key}" },
154+
new { HttpMethod = "PUT", GroupName, RelativePath = "api/Products/{key}" },
155+
new { HttpMethod = "GET", GroupName, RelativePath = "api/Products/{key}/Supplier" },
156+
new { HttpMethod = "DELETE", GroupName, RelativePath = "api/Products/{key}/supplier/{relatedKey}/$ref" },
157+
new { HttpMethod = "GET", GroupName, RelativePath = "api/Products/{key}/supplier/$ref" },
158+
new { HttpMethod = "PUT", GroupName, RelativePath = "api/Products/{key}/supplier/$ref" },
159+
new { HttpMethod = "GET", GroupName, RelativePath = "api/Suppliers" },
160+
new { HttpMethod = "POST", GroupName, RelativePath = "api/Suppliers" },
161+
new { HttpMethod = "DELETE", GroupName, RelativePath = "api/Suppliers/{key}" },
162+
new { HttpMethod = "GET", GroupName, RelativePath = "api/Suppliers/{key}" },
163+
new { HttpMethod = "GET", GroupName, RelativePath = "api/Suppliers/$count" },
164+
new { HttpMethod = "PATCH", GroupName, RelativePath = "api/Suppliers/{key}" },
165+
new { HttpMethod = "PUT", GroupName, RelativePath = "api/Suppliers/{key}" },
166+
new { HttpMethod = "GET", GroupName, RelativePath = "api/Suppliers/{key}/Products" },
167+
new { HttpMethod = "DELETE", GroupName, RelativePath = "api/Suppliers/{key}/products/{relatedKey}/$ref" },
168+
new { HttpMethod = "PUT", GroupName, RelativePath = "api/Suppliers/{key}/products/$ref" },
169+
};
131170

132171
PrintGroup( items );
133172
group.GroupName.Should().Be( GroupName );
134-
items.Should().BeEquivalentTo(
135-
new[]
136-
{
137-
new { HttpMethod = "GET", GroupName, RelativePath = "api/GetSalesTaxRate(PostalCode={postalCode})" },
138-
new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders" },
139-
new { HttpMethod = "POST", GroupName, RelativePath = "api/Orders" },
140-
new { HttpMethod = "DELETE", GroupName, RelativePath = "api/Orders/{key}" },
141-
new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders/{key}" },
142-
new { HttpMethod = "PATCH", GroupName, RelativePath = "api/Orders/{key}" },
143-
new { HttpMethod = "POST", GroupName, RelativePath = "api/Orders/{key}/Rate" },
144-
new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders/$count" },
145-
new { HttpMethod = "GET", GroupName, RelativePath = "api/Orders/MostExpensive" },
146-
new { HttpMethod = "GET", GroupName, RelativePath = "api/People" },
147-
new { HttpMethod = "POST", GroupName, RelativePath = "api/People" },
148-
new { HttpMethod = "GET", GroupName, RelativePath = "api/People/{key}" },
149-
new { HttpMethod = "POST", GroupName, RelativePath = "api/People/{key}/Promote" },
150-
new { HttpMethod = "GET", GroupName, RelativePath = "api/People/$count" },
151-
new { HttpMethod = "GET", GroupName, RelativePath = "api/People/NewHires(Since={since})" },
152-
new { HttpMethod = "GET", GroupName, RelativePath = "api/Products" },
153-
new { HttpMethod = "POST", GroupName, RelativePath = "api/Products" },
154-
new { HttpMethod = "DELETE", GroupName, RelativePath = "api/Products/{key}" },
155-
new { HttpMethod = "GET", GroupName, RelativePath = "api/Products/{key}" },
156-
new { HttpMethod = "PATCH", GroupName, RelativePath = "api/Products/{key}" },
157-
new { HttpMethod = "PUT", GroupName, RelativePath = "api/Products/{key}" },
158-
new { HttpMethod = "GET", GroupName, RelativePath = "api/Products/{key}/Supplier" },
159-
new { HttpMethod = "DELETE", GroupName, RelativePath = "api/Products/{key}/supplier/{relatedKey}/$ref" },
160-
new { HttpMethod = "GET", GroupName, RelativePath = "api/Products/{key}/supplier/$ref" },
161-
new { HttpMethod = "PUT", GroupName, RelativePath = "api/Products/{key}/supplier/$ref" },
162-
new { HttpMethod = "GET", GroupName, RelativePath = "api/Suppliers" },
163-
new { HttpMethod = "POST", GroupName, RelativePath = "api/Suppliers" },
164-
new { HttpMethod = "DELETE", GroupName, RelativePath = "api/Suppliers/{key}" },
165-
new { HttpMethod = "GET", GroupName, RelativePath = "api/Suppliers/{key}" },
166-
new { HttpMethod = "PATCH", GroupName, RelativePath = "api/Suppliers/{key}" },
167-
new { HttpMethod = "PUT", GroupName, RelativePath = "api/Suppliers/{key}" },
168-
new { HttpMethod = "GET", GroupName, RelativePath = "api/Suppliers/{key}/Products" },
169-
new { HttpMethod = "DELETE", GroupName, RelativePath = "api/Suppliers/{key}/products/{relatedKey}/$ref" },
170-
new { HttpMethod = "PUT", GroupName, RelativePath = "api/Suppliers/{key}/products/$ref" },
171-
},
172-
options => options.ExcludingMissingMembers() );
173+
items.Should().BeEquivalentTo( expected, options => options.ExcludingMissingMembers() );
173174
}
174175

175176
private void PrintGroup( IReadOnlyList<ApiDescription> items )

0 commit comments

Comments
 (0)