Skip to content

Commit 3a2e993

Browse files
author
Chris Santero
committed
Create action filter that resolves an IQueryable asynchronously
1 parent cfcfa46 commit 3a2e993

File tree

9 files changed

+353
-0
lines changed

9 files changed

+353
-0
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Data.Entity;
4+
using System.Data.Entity.Infrastructure;
5+
using System.Linq;
6+
using System.Net;
7+
using System.Net.Http;
8+
using System.Net.Http.Formatting;
9+
using System.Text;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
using System.Web.Http.Controllers;
13+
using System.Web.Http.Filters;
14+
using FluentAssertions;
15+
using JSONAPI.EntityFramework.ActionFilters;
16+
using JSONAPI.EntityFramework.Tests.Helpers;
17+
using Microsoft.VisualStudio.TestTools.UnitTesting;
18+
using Moq;
19+
20+
namespace JSONAPI.EntityFramework.Tests.ActionFilters
21+
{
22+
[TestClass]
23+
public class EnumerateQueryableAsyncAttributeTests
24+
{
25+
public class Dummy
26+
{
27+
public string Id { get; set; }
28+
29+
public string Name { get; set; }
30+
}
31+
32+
private IQueryable<Dummy> _fixtures;
33+
34+
[TestInitialize]
35+
public void SetupFixtures()
36+
{
37+
_fixtures = new List<Dummy>()
38+
{
39+
new Dummy
40+
{
41+
Id = "1",
42+
Name = "Blue"
43+
},
44+
new Dummy
45+
{
46+
Id = "2",
47+
Name = "Red"
48+
},
49+
new Dummy
50+
{
51+
Id = "3",
52+
Name = "Green"
53+
}
54+
}.AsQueryable();
55+
}
56+
57+
private HttpActionExecutedContext CreateActionExecutedContext(IDbAsyncEnumerator<Dummy> asyncEnumerator)
58+
{
59+
var mockSet = new Mock<DbSet<Dummy>>();
60+
mockSet.As<IDbAsyncEnumerable<Dummy>>()
61+
.Setup(m => m.GetAsyncEnumerator())
62+
.Returns(asyncEnumerator);
63+
64+
mockSet.As<IQueryable<Dummy>>()
65+
.Setup(m => m.Provider)
66+
.Returns(new TestDbAsyncQueryProvider<Dummy>(_fixtures.Provider));
67+
68+
mockSet.As<IQueryable<Dummy>>().Setup(m => m.Expression).Returns(_fixtures.Expression);
69+
mockSet.As<IQueryable<Dummy>>().Setup(m => m.ElementType).Returns(_fixtures.ElementType);
70+
mockSet.As<IQueryable<Dummy>>().Setup(m => m.GetEnumerator()).Returns(_fixtures.GetEnumerator());
71+
72+
var formatter = new JsonMediaTypeFormatter();
73+
74+
var httpContent = new ObjectContent(typeof(IQueryable<Dummy>), mockSet.Object, formatter);
75+
76+
return new HttpActionExecutedContext
77+
{
78+
ActionContext = new HttpActionContext
79+
{
80+
ControllerContext = new HttpControllerContext
81+
{
82+
Request = new HttpRequestMessage(HttpMethod.Get, "http://api.example.com/dummies")
83+
}
84+
},
85+
Response = new HttpResponseMessage(HttpStatusCode.OK)
86+
{
87+
Content = httpContent
88+
}
89+
};
90+
}
91+
92+
[TestMethod]
93+
public async Task ResolvesQueryable()
94+
{
95+
var actionFilter = new EnumerateQueryableAsyncAttribute();
96+
97+
var context = CreateActionExecutedContext(new TestDbAsyncEnumerator<Dummy>(_fixtures.GetEnumerator()));
98+
99+
await actionFilter.OnActionExecutedAsync(context, new CancellationToken());
100+
101+
var objectContent = context.Response.Content as ObjectContent;
102+
objectContent.Should().NotBeNull();
103+
104+
var array = objectContent.Value as Dummy[];
105+
array.Should().NotBeNull();
106+
array.Length.Should().Be(3);
107+
array[0].Id.Should().Be("1");
108+
array[1].Id.Should().Be("2");
109+
array[2].Id.Should().Be("3");
110+
}
111+
112+
[TestMethod]
113+
public void CancelsProperly()
114+
{
115+
var actionFilter = new EnumerateQueryableAsyncAttribute();
116+
117+
var context = CreateActionExecutedContext(new WaitsUntilCancellationDbAsyncEnumerator<Dummy>(1000, _fixtures.GetEnumerator()));
118+
119+
var cts = new CancellationTokenSource();
120+
cts.CancelAfter(300);
121+
122+
Func<Task> action = async () =>
123+
{
124+
await actionFilter.OnActionExecutedAsync(context, cts.Token);
125+
};
126+
action.ShouldThrow<TaskCanceledException>();
127+
}
128+
}
129+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System.Collections.Generic;
2+
using System.Data.Entity.Infrastructure;
3+
using System.Linq;
4+
using System.Linq.Expressions;
5+
6+
namespace JSONAPI.EntityFramework.Tests.Helpers
7+
{
8+
internal class TestDbAsyncEnumerable<T> : EnumerableQuery<T>, IDbAsyncEnumerable<T>, IQueryable<T>
9+
{
10+
public TestDbAsyncEnumerable(IEnumerable<T> enumerable)
11+
: base(enumerable)
12+
{ }
13+
14+
public TestDbAsyncEnumerable(Expression expression)
15+
: base(expression)
16+
{ }
17+
18+
public IDbAsyncEnumerator<T> GetAsyncEnumerator()
19+
{
20+
return new TestDbAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
21+
}
22+
23+
IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator()
24+
{
25+
return GetAsyncEnumerator();
26+
}
27+
28+
IQueryProvider IQueryable.Provider
29+
{
30+
get { return new TestDbAsyncQueryProvider<T>(this); }
31+
}
32+
}
33+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.Collections.Generic;
2+
using System.Data.Entity.Infrastructure;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
6+
namespace JSONAPI.EntityFramework.Tests.Helpers
7+
{
8+
internal class TestDbAsyncEnumerator<T> : IDbAsyncEnumerator<T>
9+
{
10+
private readonly IEnumerator<T> _inner;
11+
12+
public TestDbAsyncEnumerator(IEnumerator<T> inner)
13+
{
14+
_inner = inner;
15+
}
16+
17+
public void Dispose()
18+
{
19+
_inner.Dispose();
20+
}
21+
22+
public Task<bool> MoveNextAsync(CancellationToken cancellationToken)
23+
{
24+
return Task.FromResult(_inner.MoveNext());
25+
}
26+
27+
public T Current
28+
{
29+
get { return _inner.Current; }
30+
}
31+
32+
object IDbAsyncEnumerator.Current
33+
{
34+
get { return Current; }
35+
}
36+
}
37+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System.Data.Entity.Infrastructure;
2+
using System.Linq;
3+
using System.Linq.Expressions;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace JSONAPI.EntityFramework.Tests.Helpers
8+
{
9+
internal class TestDbAsyncQueryProvider<TEntity> : IDbAsyncQueryProvider
10+
{
11+
private readonly IQueryProvider _inner;
12+
13+
internal TestDbAsyncQueryProvider(IQueryProvider inner)
14+
{
15+
_inner = inner;
16+
}
17+
18+
public IQueryable CreateQuery(Expression expression)
19+
{
20+
return new TestDbAsyncEnumerable<TEntity>(expression);
21+
}
22+
23+
public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
24+
{
25+
return new TestDbAsyncEnumerable<TElement>(expression);
26+
}
27+
28+
public object Execute(Expression expression)
29+
{
30+
return _inner.Execute(expression);
31+
}
32+
33+
public TResult Execute<TResult>(Expression expression)
34+
{
35+
return _inner.Execute<TResult>(expression);
36+
}
37+
38+
public Task<object> ExecuteAsync(Expression expression, CancellationToken cancellationToken)
39+
{
40+
return Task.FromResult(Execute(expression));
41+
}
42+
43+
public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
44+
{
45+
return Task.FromResult(Execute<TResult>(expression));
46+
}
47+
}
48+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System.Collections.Generic;
2+
using System.Data.Entity.Infrastructure;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
6+
namespace JSONAPI.EntityFramework.Tests.Helpers
7+
{
8+
internal class WaitsUntilCancellationDbAsyncEnumerator<T> : IDbAsyncEnumerator<T>
9+
{
10+
private readonly int _timeout;
11+
private readonly IEnumerator<T> _inner;
12+
13+
public WaitsUntilCancellationDbAsyncEnumerator(int timeout, IEnumerator<T> inner)
14+
{
15+
_timeout = timeout;
16+
_inner = inner;
17+
}
18+
19+
public void Dispose()
20+
{
21+
_inner.Dispose();
22+
}
23+
24+
public async Task<bool> MoveNextAsync(CancellationToken cancellationToken)
25+
{
26+
await Task.Delay(_timeout, cancellationToken);
27+
28+
return _inner.MoveNext();
29+
}
30+
31+
public T Current
32+
{
33+
get { return _inner.Current; }
34+
}
35+
36+
object IDbAsyncEnumerator.Current
37+
{
38+
get { return Current; }
39+
}
40+
}
41+
}

JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,19 @@
4545
<Reference Include="EntityFramework.SqlServer">
4646
<HintPath>..\packages\EntityFramework.6.1.1\lib\net45\EntityFramework.SqlServer.dll</HintPath>
4747
</Reference>
48+
<Reference Include="FluentAssertions, Version=3.2.2.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL">
49+
<SpecificVersion>False</SpecificVersion>
50+
<HintPath>..\packages\FluentAssertions.3.2.2\lib\net45\FluentAssertions.dll</HintPath>
51+
</Reference>
52+
<Reference Include="FluentAssertions.Core, Version=3.2.2.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL">
53+
<SpecificVersion>False</SpecificVersion>
54+
<HintPath>..\packages\FluentAssertions.3.2.2\lib\net45\FluentAssertions.Core.dll</HintPath>
55+
</Reference>
4856
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
57+
<Reference Include="Moq, Version=4.2.1409.1722, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
58+
<SpecificVersion>False</SpecificVersion>
59+
<HintPath>..\packages\Moq.4.2.1409.1722\lib\net40\Moq.dll</HintPath>
60+
</Reference>
4961
<Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
5062
<SpecificVersion>False</SpecificVersion>
5163
<HintPath>..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll</HintPath>
@@ -63,15 +75,25 @@
6375
</Reference>
6476
<Reference Include="System.Runtime.Serialization" />
6577
<Reference Include="System.Security" />
78+
<Reference Include="System.Web.Http, Version=5.2.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
79+
<SpecificVersion>False</SpecificVersion>
80+
<HintPath>..\packages\Microsoft.AspNet.WebApi.Core.5.2.2\lib\net45\System.Web.Http.dll</HintPath>
81+
</Reference>
6682
<Reference Include="System.Xml" />
83+
<Reference Include="System.Xml.Linq" />
6784
</ItemGroup>
6885
<ItemGroup>
6986
<CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies">
7087
<Visible>False</Visible>
7188
</CodeAnalysisDependentAssemblyPaths>
7289
</ItemGroup>
7390
<ItemGroup>
91+
<Compile Include="ActionFilters\EnumerateQueryableAsyncAttributeTests.cs" />
7492
<Compile Include="EntityConverterTests.cs" />
93+
<Compile Include="Helpers\TestDbAsyncEnumerable.cs" />
94+
<Compile Include="Helpers\WaitsUntilCancellationDbAsyncEnumerator.cs" />
95+
<Compile Include="Helpers\TestDbAsyncEnumerator.cs" />
96+
<Compile Include="Helpers\TestDbAsyncQueryProvider.cs" />
7597
<Compile Include="Models\Author.cs" />
7698
<Compile Include="Models\Comment.cs" />
7799
<Compile Include="Models\TestEntities.cs" />
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<packages>
33
<package id="EntityFramework" version="6.1.1" targetFramework="net45" />
4+
<package id="FluentAssertions" version="3.2.2" targetFramework="net45" />
45
<package id="Microsoft.AspNet.WebApi.Client" version="5.2.2" targetFramework="net45" />
6+
<package id="Microsoft.AspNet.WebApi.Core" version="5.2.2" targetFramework="net45" />
7+
<package id="Moq" version="4.2.1409.1722" targetFramework="net45" />
58
<package id="Newtonsoft.Json" version="6.0.6" targetFramework="net45" />
69
</packages>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System;
2+
using System.Data.Entity;
3+
using System.Linq;
4+
using System.Net.Http;
5+
using System.Reflection;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using System.Web.Http.Filters;
9+
10+
namespace JSONAPI.EntityFramework.ActionFilters
11+
{
12+
public class EnumerateQueryableAsyncAttribute : ActionFilterAttribute
13+
{
14+
private readonly Lazy<MethodInfo> _toArrayAsyncMethod = new Lazy<MethodInfo>(() =>
15+
typeof(QueryableExtensions).GetMethods().FirstOrDefault(x => x.Name == "ToArrayAsync" && x.GetParameters().Count() == 2));
16+
17+
public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
18+
{
19+
if (actionExecutedContext.Response != null)
20+
{
21+
var objectContent = actionExecutedContext.Response.Content as ObjectContent;
22+
if (objectContent != null)
23+
{
24+
var objectType = objectContent.ObjectType;
25+
if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IQueryable<>))
26+
{
27+
var queryableElementType = objectType.GenericTypeArguments[0];
28+
var openToArrayAsyncMethod = _toArrayAsyncMethod.Value;
29+
var toArrayAsyncMethod = openToArrayAsyncMethod.MakeGenericMethod(queryableElementType);
30+
var invocation = (dynamic)toArrayAsyncMethod.Invoke(null, new[] { objectContent.Value, cancellationToken });
31+
32+
var resultArray = await invocation;
33+
actionExecutedContext.Response.Content = new ObjectContent(resultArray.GetType(), resultArray, objectContent.Formatter);
34+
}
35+
}
36+
}
37+
}
38+
}
39+
}

JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
<Reference Include="System.Xml" />
7070
</ItemGroup>
7171
<ItemGroup>
72+
<Compile Include="ActionFilters\EnumerateQueryableAsyncAttribute.cs" />
7273
<Compile Include="EntityFrameworkMaterializer.cs" />
7374
<Compile Include="EntityFrameworkMaterializer_Util.cs" />
7475
<Compile Include="Http\ApiController.cs" />

0 commit comments

Comments
 (0)