Skip to content

Commit 5296f05

Browse files
author
Chris Martinez
committed
Added support for OPTIONS on the controller backing the $metadata endpoint
1 parent 8556f44 commit 5296f05

File tree

4 files changed

+219
-14
lines changed

4 files changed

+219
-14
lines changed
Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
namespace Microsoft.Web.OData.Controllers
22
{
33
using Http;
4+
using Http.Versioning;
45
using System;
6+
using System.Collections.Generic;
7+
using System.Diagnostics.Contracts;
8+
using System.Linq;
9+
using System.Net.Http;
10+
using System.Net.Http.Headers;
511
using System.Web.Http;
12+
using System.Web.Http.Controllers;
613
using System.Web.OData;
14+
using static Microsoft.OData.Core.ODataConstants;
15+
using static Microsoft.OData.Core.ODataUtils;
16+
using static Microsoft.OData.Core.ODataVersion;
17+
using static System.Net.HttpStatusCode;
18+
using static System.String;
719

820
/// <summary>
921
/// Represents a <see cref="ApiController">controller</see> for generating versioned OData service and metadata documents.
@@ -12,5 +24,133 @@
1224
[ApiVersionNeutral]
1325
public class VersionedMetadataController : MetadataController
1426
{
27+
private sealed class DiscoveredApiVersions
28+
{
29+
private const string ValueSeparator = ", ";
30+
31+
internal DiscoveredApiVersions( IEnumerable<ApiVersionModel> models )
32+
{
33+
Contract.Requires( models != null );
34+
35+
var supported = new HashSet<ApiVersion>();
36+
var deprecated = new HashSet<ApiVersion>();
37+
38+
foreach ( var model in models )
39+
{
40+
foreach ( var version in model.SupportedApiVersions )
41+
{
42+
supported.Add( version );
43+
}
44+
45+
foreach ( var version in model.DeprecatedApiVersions )
46+
{
47+
deprecated.Add( version );
48+
}
49+
}
50+
51+
if ( supported.Count > 0 )
52+
{
53+
deprecated.ExceptWith( supported );
54+
SupportedApiVersions = Join( ValueSeparator, supported.OrderBy( v => v ).Select( v => v.ToString() ) );
55+
}
56+
57+
if ( deprecated.Count > 0 )
58+
{
59+
DeprecatedApiVersions = Join( ValueSeparator, deprecated.OrderBy( v => v ).Select( v => v.ToString() ) );
60+
}
61+
}
62+
63+
public string SupportedApiVersions { get; }
64+
65+
public string DeprecatedApiVersions { get; }
66+
}
67+
68+
private const string ApiSupportedVersions = "api-supported-versions";
69+
private const string ApiDeprecatedVersions = "api-deprecated-versions";
70+
private readonly Lazy<DiscoveredApiVersions> discovered;
71+
72+
/// <summary>
73+
/// Initializes a new instance of the <see cref="VersionedMetadataController"/> class.
74+
/// </summary>
75+
public VersionedMetadataController()
76+
{
77+
discovered = new Lazy<DiscoveredApiVersions>( () => new DiscoveredApiVersions( DiscoverODataApiVersions() ) );
78+
}
79+
80+
/// <summary>
81+
/// Handles a request for the HTTP OPTIONS method.
82+
/// </summary>
83+
/// <returns>A <see cref="IHttpActionResult">result</see> containing the response to the request.</returns>
84+
/// <remarks>When a request is made with OPTIONS /$metadata, then this method will return the following
85+
/// HTTP headers:
86+
/// <list type="table">
87+
/// <listheader>
88+
/// <term>Header Name</term>
89+
/// <description>Description</description>
90+
/// </listheader>
91+
/// <item>
92+
/// <term>OData-Version</term>
93+
/// <description>The OData version supported by the endpoint.</description>
94+
/// </item>
95+
/// <item>
96+
/// <term>api-supported-versions</term>
97+
/// <description>A comma-separated list of all supported API versions, if any.</description>
98+
/// </item>
99+
/// <item>
100+
/// <term>api-deprecated-versions</term>
101+
/// <description>A comma-separated list of all supported API versions, if any.</description>
102+
/// </item>
103+
/// </list>
104+
/// </remarks>
105+
[HttpOptions]
106+
public virtual IHttpActionResult GetOptions()
107+
{
108+
var response = new HttpResponseMessage( OK );
109+
var headers = response.Headers;
110+
111+
response.Content = new StringContent( Empty );
112+
response.Content.Headers.Add( "Allow", new[] { "GET", "OPTIONS" } );
113+
response.Content.Headers.ContentType = null;
114+
headers.Add( ODataVersionHeader, ODataVersionToString( V4 ) );
115+
ReportApiVersions( headers );
116+
117+
return ResponseMessage( response );
118+
}
119+
120+
private DiscoveredApiVersions Discovered => discovered.Value;
121+
122+
private void ReportApiVersions( HttpHeaders headers )
123+
{
124+
Contract.Requires( headers != null );
125+
126+
var value = Discovered.SupportedApiVersions;
127+
128+
if ( value != null )
129+
{
130+
headers.Add( ApiSupportedVersions, value );
131+
}
132+
133+
value = Discovered.DeprecatedApiVersions;
134+
135+
if ( value != null )
136+
{
137+
headers.Add( ApiDeprecatedVersions, value );
138+
}
139+
}
140+
141+
private IEnumerable<ApiVersionModel> DiscoverODataApiVersions()
142+
{
143+
Contract.Ensures( Contract.Result<IEnumerable<ApiVersionModel>>() != null );
144+
145+
var services = Configuration.Services;
146+
var assembliesResolver = services.GetAssembliesResolver();
147+
var typeResolver = services.GetHttpControllerTypeResolver();
148+
var controllerTypes = typeResolver.GetControllerTypes( assembliesResolver );
149+
150+
return from controllerType in controllerTypes
151+
where controllerType.IsODataController()
152+
let descriptor = new HttpControllerDescriptor( Configuration, string.Empty, controllerType )
153+
select descriptor.GetApiVersionModel();
154+
}
15155
}
16-
}
156+
}

src/Microsoft.AspNet.OData.Versioning/Web.OData/Routing/VersionedMetadataRoutingConvention.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
namespace Microsoft.Web.OData.Routing
22
{
3+
using Controllers;
34
using System.Diagnostics.CodeAnalysis;
45
using System.Linq;
56
using System.Net.Http;
67
using System.Web.Http.Controllers;
7-
using System.Web.OData;
88
using System.Web.OData.Routing;
99
using System.Web.OData.Routing.Conventions;
10+
using static System.Net.Http.HttpMethod;
1011

1112
/// <summary>
1213
/// Represents the <see cref="IODataRoutingConvention">OData routing convention</see> for versioned service and metadata documents.
@@ -43,12 +44,23 @@ public virtual string SelectAction( ODataPath odataPath, HttpControllerContext c
4344

4445
if ( odataPath.PathTemplate == "~" )
4546
{
46-
return nameof( MetadataController.GetServiceDocument );
47+
return nameof( VersionedMetadataController.GetServiceDocument );
4748
}
4849

49-
if ( odataPath.PathTemplate == "~/$metadata" )
50+
if ( odataPath.PathTemplate != "~/$metadata" )
5051
{
51-
return nameof( MetadataController.GetMetadata );
52+
return null;
53+
}
54+
55+
var method = controllerContext.Request.Method;
56+
57+
if ( method == Get )
58+
{
59+
return nameof( VersionedMetadataController.GetMetadata );
60+
}
61+
else if ( method == Options )
62+
{
63+
return nameof( VersionedMetadataController.GetOptions );
5264
}
5365

5466
return null;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
namespace Microsoft.Web.OData.Controllers
2+
{
3+
using FluentAssertions;
4+
using Http;
5+
using Moq;
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using System.Web.Http;
12+
using System.Web.Http.Dispatcher;
13+
using System.Web.OData;
14+
using Xunit;
15+
16+
public class VersionedMetadataControllerTest
17+
{
18+
[ApiVersion( "1.0" )]
19+
[ApiVersion( "2.0" )]
20+
private sealed class Controller1 : ODataController
21+
{
22+
}
23+
24+
[ApiVersion( "2.0", Deprecated = true )]
25+
[ApiVersion( "3.0-Beta", Deprecated = true )]
26+
[ApiVersion( "3.0" )]
27+
private sealed class Controller2 : ODataController
28+
{
29+
}
30+
31+
[Fact]
32+
public async Task options_should_return_expected_headers()
33+
{
34+
// arrange
35+
var configuration = new HttpConfiguration();
36+
var controllerTypeResolver = new Mock<IHttpControllerTypeResolver>();
37+
var controllerTypes = new List<Type>() { typeof( Controller1 ), typeof( Controller2 ) };
38+
39+
controllerTypeResolver.Setup( ctr => ctr.GetControllerTypes( It.IsAny<IAssembliesResolver>() ) ).Returns( controllerTypes );
40+
configuration.Services.Replace( typeof( IHttpControllerTypeResolver ), controllerTypeResolver.Object );
41+
42+
var metadata = new VersionedMetadataController() { Configuration = configuration };
43+
44+
// act
45+
var response = await metadata.GetOptions().ExecuteAsync( CancellationToken.None );
46+
47+
// assert
48+
response.Headers.GetValues( "OData-Version" ).Single().Should().Be( "4.0" );
49+
response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" );
50+
response.Headers.GetValues( "api-deprecated-versions" ).Single().Should().Be( "3.0-Beta" );
51+
response.Content.Headers.Allow.Should().BeEquivalentTo( "GET", "OPTIONS" );
52+
}
53+
}
54+
}

test/Microsoft.AspNet.OData.Versioning.Tests/Web.OData/Routing/VersionedMetadataRoutingConventionTest.cs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@
99
using System.Web.OData.Routing;
1010
using Xunit;
1111

12-
/// <summary>
13-
/// Provides unit tests for <see cref="VersionedMetadataRoutingConvention"/>.
14-
/// </summary>
1512
public class VersionedMetadataRoutingConventionTest
1613
{
1714
public static IEnumerable<object[]> SelectControllerData
@@ -29,10 +26,11 @@ public static IEnumerable<object[]> SelectActionData
2926
{
3027
get
3128
{
32-
yield return new object[] { new ODataPath(), "GetServiceDocument" };
33-
yield return new object[] { new ODataPath( new MetadataPathSegment() ), "GetMetadata" };
34-
yield return new object[] { new ODataPath( new EntitySetPathSegment( "Tests" ) ), null };
35-
yield return new object[] { new ODataPath( new EntitySetPathSegment( "Tests" ), new KeyValuePathSegment( "42" ) ), null };
29+
yield return new object[] { new ODataPath(), "GET", "GetServiceDocument" };
30+
yield return new object[] { new ODataPath( new MetadataPathSegment() ), "GET", "GetMetadata" };
31+
yield return new object[] { new ODataPath( new MetadataPathSegment() ), "OPTIONS", "GetOptions" };
32+
yield return new object[] { new ODataPath( new EntitySetPathSegment( "Tests" ) ), "GET", null };
33+
yield return new object[] { new ODataPath( new EntitySetPathSegment( "Tests" ), new KeyValuePathSegment( "42" ) ), "GET", null };
3634
}
3735
}
3836

@@ -53,10 +51,11 @@ public void select_controller_should_return_expected_name( ODataPath odataPath,
5351

5452
[Theory]
5553
[MemberData( nameof( SelectActionData ) )]
56-
public void select_action_should_return_expected_name( ODataPath odataPath, string expected )
54+
public void select_action_should_return_expected_name( ODataPath odataPath, string verb, string expected )
5755
{
5856
// arrange
59-
var controllerContext = new HttpControllerContext();
57+
var request = new HttpRequestMessage( new HttpMethod( verb ), "http://localhost/$metadata" );
58+
var controllerContext = new HttpControllerContext() { Request = request };
6059
var actionMap = new Mock<ILookup<string, HttpActionDescriptor>>().Object;
6160
var routingConvention = new VersionedMetadataRoutingConvention();
6261

0 commit comments

Comments
 (0)