Skip to content

Commit 7be76be

Browse files
Support OData batching. Fixes #847. Relates to #720.
1 parent ccea5ba commit 7be76be

17 files changed

+809
-17
lines changed

src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/HttpServerFixture.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ protected virtual void OnAddMvcApiVersioning( MvcApiVersioningOptions options )
3232

3333
protected virtual void OnAddApiVersioning( IApiVersioningBuilder builder ) { }
3434

35+
protected virtual void OnBuildApplication( IApplicationBuilder app ) =>
36+
app.UseRouting().UseEndpoints( OnConfigureEndpoints );
37+
3538
protected virtual void OnConfigureEndpoints( IEndpointRouteBuilder endpoints ) => endpoints.MapControllers();
3639

3740
private static string GenerateEndpointDirectedGraph( IServiceProvider services )
@@ -70,7 +73,7 @@ private TestServer CreateServer()
7073
{
7174
var builder = new WebHostBuilder()
7275
.ConfigureServices( OnDefaultConfigureServices )
73-
.Configure( app => app.UseRouting().UseEndpoints( OnConfigureEndpoints ) )
76+
.Configure( OnBuildApplication )
7477
.UseContentRoot( GetContentRoot() );
7578

7679
return new TestServer( builder );

src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/BasicFixture.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace Asp.Versioning.OData.Basic;
44

55
using Asp.Versioning.OData;
66
using Asp.Versioning.OData.Basic.Controllers;
7+
using Microsoft.AspNetCore.Builder;
78

89
public class BasicFixture : ODataFixture
910
{
@@ -20,5 +21,11 @@ protected override void OnAddApiVersioning( ApiVersioningOptions options ) =>
2021
options.ReportApiVersions = true;
2122

2223
protected override void OnEnableOData( ODataApiVersioningOptions options ) =>
23-
options.AddRouteComponents( "api" ).AddRouteComponents( "v{version:apiVersion}" );
24+
options.AddRouteComponents( "api" )
25+
.AddRouteComponents( "v{version:apiVersion}" );
26+
27+
protected override void OnBuildApplication( IApplicationBuilder app ) =>
28+
app.UseVersionedODataBatching()
29+
.UseRouting()
30+
.UseEndpoints( OnConfigureEndpoints );
2431
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
namespace Asp.Versioning.OData.Basic;
4+
5+
using Asp.Versioning;
6+
using System.Net.Http.Formatting;
7+
using System.Net.Http.Headers;
8+
using static System.Net.Http.HttpMethod;
9+
10+
[Collection( "OData" + nameof( BasicTestCollection ) )]
11+
public abstract class BatchAcceptanceTest : AcceptanceTest
12+
{
13+
protected BatchAcceptanceTest( ODataFixture fixture ) : base( fixture ) { }
14+
15+
protected HttpRequestMessage NewBatch(
16+
string requestUri,
17+
HttpContent request,
18+
params HttpContent[] otherRequests )
19+
{
20+
var content = new MultipartContent( "mixed" ) { request };
21+
22+
for ( var i = 0; i < otherRequests.Length; i++ )
23+
{
24+
content.Add( otherRequests[i] );
25+
}
26+
27+
return new( Post, new Uri( Client.BaseAddress, requestUri ) ) { Content = content };
28+
}
29+
30+
protected HttpMessageContent NewGet( string requestUri ) => NewRequest<object>( Get, requestUri );
31+
32+
protected HttpMessageContent NewDelete( string requestUri ) => NewRequest<object>( Delete, requestUri );
33+
34+
protected HttpMessageContent NewPut<T>( string requestUri, T entity )
35+
where T : class => NewRequest( Put, requestUri, entity );
36+
37+
protected HttpMessageContent NewRequest<T>( HttpMethod method, string requestUri, T entity = default )
38+
where T : class
39+
{
40+
var request = new HttpRequestMessage( method, new Uri( Client.BaseAddress, requestUri ) );
41+
42+
if ( !Equals( entity, default( T ) ) )
43+
{
44+
request.Content = new ObjectContent<T>(
45+
entity,
46+
new JsonMediaTypeFormatter(),
47+
JsonMediaTypeFormatter.DefaultMediaType );
48+
}
49+
50+
var content = new HttpMessageContent( request )
51+
{
52+
Headers =
53+
{
54+
ContentType = MediaTypeHeaderValue.Parse( "application/http" ),
55+
},
56+
};
57+
58+
content.Headers.TryAddWithoutValidation( "Content-Transfer-Encoding", "binary" );
59+
60+
return content;
61+
}
62+
63+
protected static async Task<T> ReadAsAsync<T>( HttpContent content, T example )
64+
{
65+
content.Headers.ContentType.Parameters.Add( new( "msgtype", "response" ) );
66+
using var response = await content.ReadAsHttpResponseMessageAsync();
67+
return await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( example );
68+
}
69+
70+
protected static async IAsyncEnumerable<HttpResponseMessage> ReadAsResponses( HttpResponseMessage response )
71+
{
72+
var multipart = await response.Content.ReadAsMultipartAsync();
73+
var contents = multipart.Contents;
74+
75+
for ( var i = 0; i < contents.Count; i++ )
76+
{
77+
var content = contents[i];
78+
content.Headers.ContentType.Parameters.Add( new( "msgtype", "response" ) );
79+
yield return await content.ReadAsHttpResponseMessageAsync();
80+
}
81+
}
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
#pragma warning disable CA2000 // Dispose objects before losing scope
4+
5+
namespace given_versioned_batch_middleware;
6+
7+
using Asp.Versioning.OData.Basic;
8+
using static System.Net.HttpStatusCode;
9+
10+
public class when_using_a_query_string : BatchAcceptanceTest
11+
{
12+
[Fact]
13+
public async Task then_2_different_versions_should_return_200()
14+
{
15+
// arrange
16+
using var request = NewBatch(
17+
"api/$batch",
18+
NewGet( "api/people/42?api-version=1.0" ),
19+
NewGet( "api/people/42?api-version=2.0" ) );
20+
21+
// act
22+
var response = await Client.SendAsync( request );
23+
24+
// assert
25+
response.IsSuccessStatusCode.Should().BeTrue();
26+
27+
var multipart = await response.Content.ReadAsMultipartAsync();
28+
var contents = multipart.Contents;
29+
30+
contents.Should().HaveCount( 2 );
31+
32+
const string NoEmailInV1 = default;
33+
var example = new { id = 0, firstName = "", lastName = "", email = "" };
34+
var person1 = await ReadAsAsync( multipart.Contents[0], example );
35+
var person2 = await ReadAsAsync( multipart.Contents[1], example );
36+
37+
person1.Should().BeEquivalentTo(
38+
new
39+
{
40+
id = 42,
41+
firstName = "Bill",
42+
lastName = "Mei",
43+
email = NoEmailInV1,
44+
} );
45+
person2.Should().BeEquivalentTo(
46+
new
47+
{
48+
id = 42,
49+
firstName = "Bill",
50+
lastName = "Mei",
51+
email = "[email protected]",
52+
} );
53+
}
54+
55+
[Fact]
56+
public async Task then_2_different_entity_sets_should_return_200()
57+
{
58+
// arrange
59+
var expected = new
60+
{
61+
id = 42,
62+
firstName = "Bill",
63+
lastName = "Mei",
64+
email = "[email protected]",
65+
phone = "555-555-5555",
66+
};
67+
using var request = NewBatch(
68+
"api/$batch",
69+
NewGet( "api/people/42?api-version=3.0" ),
70+
NewGet( "api/orders/42?api-version=1.0" ) );
71+
72+
// act
73+
var response = await Client.SendAsync( request );
74+
75+
// assert
76+
response.IsSuccessStatusCode.Should().BeTrue();
77+
78+
var multipart = await response.Content.ReadAsMultipartAsync();
79+
var contents = multipart.Contents;
80+
81+
contents.Should().HaveCount( 2 );
82+
83+
var person = await ReadAsAsync( multipart.Contents[0], expected );
84+
var order = await ReadAsAsync( multipart.Contents[1], new { id = 0, customer = "" } );
85+
86+
person.Should().BeEquivalentTo( expected );
87+
order.Should().BeEquivalentTo( new { id = 42, customer = "Bill Mei" } );
88+
}
89+
90+
[Fact]
91+
public async Task then_explicit_versions_should_succeed()
92+
{
93+
// arrange
94+
var customer = new
95+
{
96+
id = 42,
97+
firstName = "Bill",
98+
lastName = "Mei",
99+
email = "[email protected]",
100+
phone = "555-555-5555",
101+
};
102+
using var request = NewBatch(
103+
"api/$batch?api-version=1.0",
104+
NewPut( "api/customers/42?api-version=3.0", customer ),
105+
NewDelete( "api/customers/42" ) );
106+
107+
// act
108+
var response = await Client.SendAsync( request );
109+
110+
// assert
111+
response.IsSuccessStatusCode.Should().BeTrue();
112+
113+
await foreach ( var subresponse in ReadAsResponses( response ) )
114+
{
115+
subresponse.StatusCode.Should().Be( NoContent );
116+
}
117+
}
118+
119+
public when_using_a_query_string( BasicFixture fixture ) : base( fixture ) { }
120+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
#pragma warning disable CA2000 // Dispose objects before losing scope
4+
5+
namespace given_versioned_batch_middleware;
6+
7+
using Asp.Versioning.OData.Basic;
8+
using static System.Net.HttpStatusCode;
9+
10+
public class when_using_a_url_segment : BatchAcceptanceTest
11+
{
12+
[Fact]
13+
public async Task then_2_different_versions_should_return_200()
14+
{
15+
// arrange
16+
using var request = NewBatch(
17+
"api/$batch",
18+
NewGet( "v1/people/42" ),
19+
NewGet( "v2/people/42" ) );
20+
21+
// act
22+
var response = await Client.SendAsync( request );
23+
24+
// assert
25+
response.IsSuccessStatusCode.Should().BeTrue();
26+
27+
var multipart = await response.Content.ReadAsMultipartAsync();
28+
var contents = multipart.Contents;
29+
30+
contents.Should().HaveCount( 2 );
31+
32+
const string NoEmailInV1 = default;
33+
var example = new { id = 0, firstName = "", lastName = "", email = "" };
34+
var person1 = await ReadAsAsync( multipart.Contents[0], example );
35+
var person2 = await ReadAsAsync( multipart.Contents[1], example );
36+
37+
person1.Should().BeEquivalentTo(
38+
new
39+
{
40+
id = 42,
41+
firstName = "Bill",
42+
lastName = "Mei",
43+
email = NoEmailInV1,
44+
} );
45+
person2.Should().BeEquivalentTo(
46+
new
47+
{
48+
id = 42,
49+
firstName = "Bill",
50+
lastName = "Mei",
51+
email = "[email protected]",
52+
} );
53+
}
54+
55+
[Fact]
56+
public async Task then_2_different_entity_sets_should_return_200()
57+
{
58+
// arrange
59+
var expected = new
60+
{
61+
id = 42,
62+
firstName = "Bill",
63+
lastName = "Mei",
64+
email = "[email protected]",
65+
phone = "555-555-5555",
66+
};
67+
using var request = NewBatch(
68+
"api/$batch",
69+
NewGet( "v3/people/42" ),
70+
NewGet( "v1/orders/42" ) );
71+
72+
// act
73+
var response = await Client.SendAsync( request );
74+
75+
// assert
76+
response.IsSuccessStatusCode.Should().BeTrue();
77+
78+
var multipart = await response.Content.ReadAsMultipartAsync();
79+
var contents = multipart.Contents;
80+
81+
contents.Should().HaveCount( 2 );
82+
83+
var person = await ReadAsAsync( multipart.Contents[0], expected );
84+
var order = await ReadAsAsync( multipart.Contents[1], new { id = 0, customer = "" } );
85+
86+
person.Should().BeEquivalentTo( expected );
87+
order.Should().BeEquivalentTo( new { id = 42, customer = "Bill Mei" } );
88+
}
89+
90+
[Fact]
91+
public async Task then_explicit_versions_should_succeed()
92+
{
93+
// arrange
94+
var customer = new
95+
{
96+
id = 42,
97+
firstName = "Bill",
98+
lastName = "Mei",
99+
email = "[email protected]",
100+
phone = "555-555-5555",
101+
};
102+
using var request = NewBatch(
103+
"v1/$batch",
104+
NewPut( "v3/customers/42", customer ),
105+
NewDelete( "v3/customers/42" ) );
106+
107+
// act
108+
var response = await Client.SendAsync( request );
109+
110+
// assert
111+
response.IsSuccessStatusCode.Should().BeTrue();
112+
113+
await foreach ( var subresponse in ReadAsResponses( response ) )
114+
{
115+
subresponse.StatusCode.Should().Be( NoContent );
116+
}
117+
}
118+
119+
public when_using_a_url_segment( BasicFixture fixture ) : base( fixture ) { }
120+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<VersionPrefix>6.0.0</VersionPrefix>
5-
<AssemblyVersion>6.0.0.0</AssemblyVersion>
4+
<VersionPrefix>6.1.0</VersionPrefix>
5+
<AssemblyVersion>6.1.0.0</AssemblyVersion>
66
<TargetFrameworks>net6.0;netcoreapp3.1</TargetFrameworks>
77
<RootNamespace>Asp.Versioning</RootNamespace>
88
<AssemblyTitle>ASP.NET Core API Versioning with OData v4.0</AssemblyTitle>

0 commit comments

Comments
 (0)