Skip to content

Commit 09ce5e8

Browse files
authored
(#184) Allow query calls to be async (#191)
1 parent cd61a24 commit 09ce5e8

File tree

2 files changed

+74
-46
lines changed

2 files changed

+74
-46
lines changed

src/CommunityToolkit.Datasync.Server/Controllers/TableController.Query.cs

+30-8
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,19 @@ public virtual async Task<IActionResult> QueryAsync(CancellationToken cancellati
7373
// to switch to in-memory processing for those queries. This is done by calling ToListAsync() on the
7474
// IQueryable. This is not ideal, but it is the only way to support all of the OData query options.
7575
IEnumerable<object>? results = null;
76-
ExecuteQueryWithClientEvaluation(dataset, ds => results = (IEnumerable<object>)queryOptions.ApplyTo(ds, querySettings));
76+
await ExecuteQueryWithClientEvaluationAsync(dataset, ds =>
77+
{
78+
results = (IEnumerable<object>)queryOptions.ApplyTo(ds, querySettings);
79+
return Task.CompletedTask;
80+
});
7781

7882
int count = 0;
7983
FilterQueryOption? filter = queryOptions.Filter;
80-
ExecuteQueryWithClientEvaluation(dataset, ds => { IQueryable<TEntity> q = (IQueryable<TEntity>)(filter?.ApplyTo(ds, new ODataQuerySettings()) ?? ds); count = q.Count(); });
84+
await ExecuteQueryWithClientEvaluationAsync(dataset, async ds =>
85+
{
86+
IQueryable<TEntity> q = (IQueryable<TEntity>)(filter?.ApplyTo(ds, new ODataQuerySettings()) ?? ds);
87+
count = await CountAsync(q, cancellationToken);
88+
});
8189

8290
PagedResult result = BuildPagedResult(queryOptions, results, count);
8391
Logger.LogInformation("Query: {Count} items being returned", result.Items.Count());
@@ -194,13 +202,13 @@ internal static string CreateNextLink(string queryString, int skip = 0, int top
194202
/// <param name="reason">The reason if the client-side evaluator throws.</param>
195203
/// <param name="clientSideEvaluator">The client-side evaluator</param>
196204
[NonAction]
197-
internal void CatchClientSideEvaluationException(Exception ex, string reason, Action clientSideEvaluator)
205+
internal async Task CatchClientSideEvaluationExceptionAsync(Exception ex, string reason, Func<Task> clientSideEvaluator)
198206
{
199207
if (IsClientSideEvaluationException(ex) || IsClientSideEvaluationException(ex.InnerException))
200208
{
201209
try
202210
{
203-
clientSideEvaluator.Invoke();
211+
await clientSideEvaluator.Invoke();
204212
}
205213
catch (Exception err)
206214
{
@@ -220,18 +228,18 @@ internal void CatchClientSideEvaluationException(Exception ex, string reason, Ac
220228
/// <param name="dataset">The dataset to be evaluated.</param>
221229
/// <param name="evaluator">The base evaluation to be performed.</param>
222230
[NonAction]
223-
internal void ExecuteQueryWithClientEvaluation(IQueryable<TEntity> dataset, Action<IQueryable<TEntity>> evaluator)
231+
internal async Task ExecuteQueryWithClientEvaluationAsync(IQueryable<TEntity> dataset, Func<IQueryable<TEntity>, Task> evaluator)
224232
{
225233
try
226234
{
227-
evaluator.Invoke(dataset);
235+
await evaluator.Invoke(dataset);
228236
}
229237
catch (Exception ex) when (!Options.DisableClientSideEvaluation)
230238
{
231-
CatchClientSideEvaluationException(ex, "executing query", () =>
239+
await CatchClientSideEvaluationExceptionAsync(ex, "executing query", async () =>
232240
{
233241
Logger.LogWarning("Error while executing query: possible client-side evaluation ({Message})", ex.InnerException?.Message ?? ex.Message);
234-
evaluator.Invoke(dataset.ToList().AsQueryable());
242+
await evaluator.Invoke(dataset.ToList().AsQueryable());
235243
});
236244
}
237245
}
@@ -245,4 +253,18 @@ internal void ExecuteQueryWithClientEvaluation(IQueryable<TEntity> dataset, Acti
245253
[SuppressMessage("Roslynator", "RCS1158:Static member in generic type should use a type parameter.")]
246254
internal static bool IsClientSideEvaluationException(Exception? ex)
247255
=> ex is not null and (InvalidOperationException or NotSupportedException);
256+
257+
/// <summary>
258+
/// This is an overridable method that calls Count() on the provided queryable. You can override
259+
/// this to calls a provider-specific count mechanism (e.g. CountAsync().
260+
/// </summary>
261+
/// <param name="query"></param>
262+
/// <param name="cancellationToken"></param>
263+
/// <returns></returns>
264+
[NonAction]
265+
public virtual Task<int> CountAsync(IQueryable<TEntity> query, CancellationToken cancellationToken)
266+
{
267+
int result = query.Count();
268+
return Task.FromResult(result);
269+
}
248270
}

tests/CommunityToolkit.Datasync.Server.Test/Controllers/TableController_Query_Tests.cs

+44-38
Original file line numberDiff line numberDiff line change
@@ -41,167 +41,173 @@ public void BuildPagedResult_NulLArg_BuildsPagedResult()
4141

4242
#region CatchClientSideEvaluationException
4343
[Fact]
44-
public void CatchClientSideEvaluationException_NotCCEE_ThrowsOriginalException()
44+
public async Task CatchClientSideEvaluationException_NotCCEE_ThrowsOriginalException()
4545
{
4646
TableController<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
4747
ApplicationException exception = new("Original exception");
4848

49-
static void evaluator() { throw new ApplicationException("In evaluator"); }
49+
static Task evaluator() { throw new ApplicationException("In evaluator"); }
5050

51-
Action act = () => controller.CatchClientSideEvaluationException(exception, "foo", evaluator);
52-
act.Should().Throw<ApplicationException>().WithMessage("Original exception");
51+
Func<Task> act = async () => await controller.CatchClientSideEvaluationExceptionAsync(exception, "foo", evaluator);
52+
(await act.Should().ThrowAsync<ApplicationException>()).WithMessage("Original exception");
5353
}
5454

5555
[Fact]
56-
public void CatchClientSideEvaluationException_NotCCEE_WithInner_ThrowsOriginalException()
56+
public async Task CatchClientSideEvaluationException_NotCCEE_WithInner_ThrowsOriginalException()
5757
{
5858
TableController<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
5959
ApplicationException exception = new("Original exception", new ApplicationException());
6060

61-
static void evaluator() { throw new ApplicationException("In evaluator"); }
61+
static Task evaluator() { throw new ApplicationException("In evaluator"); }
6262

63-
Action act = () => controller.CatchClientSideEvaluationException(exception, "foo", evaluator);
64-
act.Should().Throw<ApplicationException>().WithMessage("Original exception");
63+
Func<Task> act = async () => await controller.CatchClientSideEvaluationExceptionAsync(exception, "foo", evaluator);
64+
(await act.Should().ThrowAsync<ApplicationException>()).WithMessage("Original exception");
6565
}
6666

6767
[Fact]
68-
public void CatchClientSideEvaluationException_CCEE_ThrowsEvaluatorException()
68+
public async Task CatchClientSideEvaluationException_CCEE_ThrowsEvaluatorException()
6969
{
7070
TableController<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
7171
NotSupportedException exception = new("Original exception", new ApplicationException("foo"));
7272

73-
static void evaluator() { throw new ApplicationException("In evaluator"); }
73+
static Task evaluator() { throw new ApplicationException("In evaluator"); }
7474

75-
Action act = () => controller.CatchClientSideEvaluationException(exception, "foo", evaluator);
76-
act.Should().Throw<ApplicationException>().WithMessage("In evaluator");
75+
Func<Task> act = async () => await controller.CatchClientSideEvaluationExceptionAsync(exception, "foo", evaluator);
76+
(await act.Should().ThrowAsync<ApplicationException>()).WithMessage("In evaluator");
7777
}
7878

7979
[Fact]
80-
public void CatchClientSideEvaluationException_CCEEInner_ThrowsEvaluatorException()
80+
public async Task CatchClientSideEvaluationException_CCEEInner_ThrowsEvaluatorException()
8181
{
8282
TableController<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
8383
ApplicationException exception = new("Original exception", new NotSupportedException("foo"));
8484

85-
static void evaluator() { throw new ApplicationException("In evaluator"); }
85+
static Task evaluator() { throw new ApplicationException("In evaluator"); }
8686

87-
Action act = () => controller.CatchClientSideEvaluationException(exception, "foo", evaluator);
88-
act.Should().Throw<ApplicationException>().WithMessage("In evaluator");
87+
Func<Task> act = async () => await controller.CatchClientSideEvaluationExceptionAsync(exception, "foo", evaluator);
88+
(await act.Should().ThrowAsync<ApplicationException>()).WithMessage("In evaluator");
8989
}
9090

9191
[Fact]
92-
public void CatchClientSideEvaluationException_CCEE_ExecutesEvaluator()
92+
public async Task CatchClientSideEvaluationException_CCEE_ExecutesEvaluator()
9393
{
9494
bool isExecuted = false;
9595
TableController<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
9696
NotSupportedException exception = new("Original exception", new ApplicationException("foo"));
97-
Action act = () => controller.CatchClientSideEvaluationException(exception, "foo", () => isExecuted = true);
98-
act.Should().NotThrow();
97+
98+
Func<Task> act = async () => await controller.CatchClientSideEvaluationExceptionAsync(exception, "foo", () => { isExecuted = true; return Task.CompletedTask; });
99+
await act.Should().NotThrowAsync();
99100
isExecuted.Should().BeTrue();
100101
}
101102

102103
[Fact]
103-
public void CatchClientSideEvaluationException_CCEEInner_ExecutesEvaluator()
104+
public async Task CatchClientSideEvaluationException_CCEEInner_ExecutesEvaluator()
104105
{
105106
bool isExecuted = false;
106107
TableController<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
107108
ApplicationException exception = new("Original exception", new NotSupportedException("foo"));
108-
Action act = () => controller.CatchClientSideEvaluationException(exception, "foo", () => isExecuted = true);
109-
act.Should().NotThrow();
109+
110+
Func<Task> act = async () => await controller.CatchClientSideEvaluationExceptionAsync(exception, "foo", () => { isExecuted = true; return Task.CompletedTask; });
111+
await act.Should().NotThrowAsync();
110112
isExecuted.Should().BeTrue();
111113
}
112114
#endregion
113115

114116
#region ExecuteQueryWithClientEvaluation
115117
[Fact]
116-
public void ExecuteQueryWithClientEvaluation_ExecutesServiceSide()
118+
public async Task ExecuteQueryWithClientEvaluation_ExecutesServiceSide()
117119
{
118120
TableController<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
119121
controller.Options.DisableClientSideEvaluation = true;
120122

121123
int evaluations = 0;
122-
void evaluator(IQueryable<InMemoryMovie> dataset)
124+
Task evaluator(IQueryable<InMemoryMovie> dataset)
123125
{
124126
evaluations++;
125127
// if (evaluations == 1) throw new NotSupportedException("Server side");
126128
// if (evaluations == 2) throw new NotSupportedException("Client side");
129+
return Task.CompletedTask;
127130
}
128131

129132
List<InMemoryMovie> dataset = [];
130133

131-
Action act = () => controller.ExecuteQueryWithClientEvaluation(dataset.AsQueryable(), evaluator);
134+
Func<Task> act = async () => await controller.ExecuteQueryWithClientEvaluationAsync(dataset.AsQueryable(), evaluator);
132135

133-
act.Should().NotThrow();
136+
await act.Should().NotThrowAsync();
134137
evaluations.Should().Be(1);
135138
}
136139

137140
[Fact]
138-
public void ExecuteQueryWithClientEvaluation_ThrowsServiceSide_WhenClientEvaluationDisabled()
141+
public async Task ExecuteQueryWithClientEvaluation_ThrowsServiceSide_WhenClientEvaluationDisabled()
139142
{
140143
TableController<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
141144
controller.Options.DisableClientSideEvaluation = true;
142145

143146
int evaluations = 0;
144147
#pragma warning disable IDE0011 // Add braces
145-
void evaluator(IQueryable<InMemoryMovie> dataset)
148+
Task evaluator(IQueryable<InMemoryMovie> dataset)
146149
{
147150
evaluations++;
148151
if (evaluations == 1) throw new NotSupportedException("Server side");
149152
if (evaluations == 2) throw new NotSupportedException("Client side");
153+
return Task.CompletedTask;
150154
}
151155
#pragma warning restore IDE0011 // Add braces
152156

153157
List<InMemoryMovie> dataset = [];
154158

155-
Action act = () => controller.ExecuteQueryWithClientEvaluation(dataset.AsQueryable(), evaluator);
159+
Func<Task> act = async () => await controller.ExecuteQueryWithClientEvaluationAsync(dataset.AsQueryable(), evaluator);
156160

157-
act.Should().Throw<NotSupportedException>().WithMessage("Server side");
161+
(await act.Should().ThrowAsync<NotSupportedException>()).WithMessage("Server side");
158162
}
159163

160164
[Fact]
161-
public void ExecuteQueryWithClientEvaluation_ExecutesClientSide_WhenClientEvaluationEnabled()
165+
public async Task ExecuteQueryWithClientEvaluation_ExecutesClientSide_WhenClientEvaluationEnabled()
162166
{
163167
TableController<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
164168
controller.Options.DisableClientSideEvaluation = false;
165169

166170
int evaluations = 0;
167171
#pragma warning disable IDE0011 // Add braces
168-
void evaluator(IQueryable<InMemoryMovie> dataset)
172+
Task evaluator(IQueryable<InMemoryMovie> dataset)
169173
{
170174
evaluations++;
171175
if (evaluations == 1) throw new NotSupportedException("Server side");
172176
//if (evaluations == 2) throw new NotSupportedException("Client side");
177+
return Task.CompletedTask;
173178
}
174179
#pragma warning restore IDE0011 // Add braces
175180

176181
List<InMemoryMovie> dataset = [];
177182

178-
Action act = () => controller.ExecuteQueryWithClientEvaluation(dataset.AsQueryable(), evaluator);
183+
Func<Task> act = async () => await controller.ExecuteQueryWithClientEvaluationAsync(dataset.AsQueryable(), evaluator);
179184

180-
act.Should().NotThrow();
185+
await act.Should().NotThrowAsync();
181186
evaluations.Should().Be(2);
182187
}
183188

184189
[Fact]
185-
public void ExecuteQueryWithClientEvaluation_ThrowsClientSide_WhenClientEvaluationEnabled()
190+
public async Task ExecuteQueryWithClientEvaluation_ThrowsClientSide_WhenClientEvaluationEnabled()
186191
{
187192
TableController<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
188193
controller.Options.DisableClientSideEvaluation = false;
189194

190195
int evaluations = 0;
191196
#pragma warning disable IDE0011 // Add braces
192-
void evaluator(IQueryable<InMemoryMovie> dataset)
197+
Task evaluator(IQueryable<InMemoryMovie> dataset)
193198
{
194199
evaluations++;
195200
if (evaluations == 1) throw new NotSupportedException("Server side", new ApplicationException("Inner exception"));
196201
if (evaluations == 2) throw new NotSupportedException("Client side");
202+
return Task.CompletedTask;
197203
}
198204
#pragma warning restore IDE0011 // Add braces
199205

200206
List<InMemoryMovie> dataset = [];
201207

202-
Action act = () => controller.ExecuteQueryWithClientEvaluation(dataset.AsQueryable(), evaluator);
208+
Func<Task> act = async () => await controller.ExecuteQueryWithClientEvaluationAsync(dataset.AsQueryable(), evaluator);
203209

204-
act.Should().Throw<NotSupportedException>().WithMessage("Client side");
210+
(await act.Should().ThrowAsync<NotSupportedException>()).WithMessage("Client side");
205211
evaluations.Should().Be(2);
206212
}
207213
#endregion

0 commit comments

Comments
 (0)