Skip to content

Commit 296e891

Browse files
author
Adrian Hall
committed
(#199) Return 400 Bad Request on a client-side evaluation.
1 parent f6bf310 commit 296e891

File tree

4 files changed

+46
-21
lines changed

4 files changed

+46
-21
lines changed

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,31 @@ public virtual async Task<IActionResult> QueryAsync(CancellationToken cancellati
7676
IQueryable<TEntity> filteredDataset = dataset.ApplyODataFilter(queryOptions.Filter, querySettings);
7777

7878
// Count the number of items within the filtered dataset - this is used when $count is requested.
79-
int filteredCount = await Repository.CountAsync(filteredDataset, cancellationToken).ConfigureAwait(false);
79+
int filteredCount;
80+
try
81+
{
82+
filteredCount = await Repository.CountAsync(filteredDataset, cancellationToken).ConfigureAwait(false);
83+
}
84+
catch (Exception ex) when (ex is InvalidOperationException or NotSupportedException)
85+
{
86+
throw new HttpException(400, "Client-side evaluation is not supported. Please ensure that the query can be translated to a server-side query.");
87+
}
8088

8189
// Now apply the OrderBy, Skip, and Top options to the dataset.
8290
IQueryable<TEntity> orderedDataset = filteredDataset
8391
.ApplyODataOrderBy(queryOptions.OrderBy, querySettings)
8492
.ApplyODataPaging(queryOptions, querySettings);
8593

8694
// Get the list of items within the dataset that need to be returned.
87-
IList<TEntity> entitiesInResultSet = await Repository.ToListAsync(orderedDataset, cancellationToken).ConfigureAwait(false);
95+
IList<TEntity> entitiesInResultSet;
96+
try
97+
{
98+
entitiesInResultSet = await Repository.ToListAsync(orderedDataset, cancellationToken).ConfigureAwait(false);
99+
}
100+
catch (Exception ex) when (ex is InvalidOperationException or NotSupportedException)
101+
{
102+
throw new HttpException(400, "Client-side evaluation is not supported. Please ensure that the query can be translated to a server-side query.");
103+
}
88104

89105
// Produce the paged result.
90106
PagedResult result = BuildPagedResult(queryOptions, entitiesInResultSet.ApplyODataSelect(queryOptions.SelectExpand, querySettings), filteredCount);

src/CommunityToolkit.Datasync.Server/Models/TableControllerOptions.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ public class TableControllerOptions
2929
/// will get a 500 Internal Server Error if they attempt to use a query that cannot
3030
/// be evaluated by the database.
3131
/// </summary>
32+
/// <remarks>
33+
/// This option is no longer used (since v9.0.0)
34+
/// </remarks>
35+
[Obsolete("Client-side evaluation is no longer supported. This option will be removed in a future release.")]
3236
public bool DisableClientSideEvaluation { get; set; }
3337

3438
/// <summary>
@@ -44,7 +48,7 @@ public class TableControllerOptions
4448
public int MaxTop
4549
{
4650
get => this._maxTop;
47-
set
51+
set
4852
{
4953
ArgumentOutOfRangeException.ThrowIfLessThan(value, 1, nameof(MaxTop));
5054
ArgumentOutOfRangeException.ThrowIfGreaterThan(value, MAX_TOP, nameof(MaxTop));
@@ -58,11 +62,11 @@ public int MaxTop
5862
public int PageSize
5963
{
6064
get => this._pageSize;
61-
set
65+
set
6266
{
6367
ArgumentOutOfRangeException.ThrowIfLessThan(value, 1, nameof(PageSize));
6468
ArgumentOutOfRangeException.ThrowIfGreaterThan(value, MAX_PAGESIZE, nameof(PageSize));
65-
this._pageSize = value;
69+
this._pageSize = value;
6670
}
6771
}
6872

tests/CommunityToolkit.Datasync.Server.Test/Helpers/LiveControllerTests.cs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public abstract class LiveControllerTests<TEntity> : BaseTest where TEntity : cl
5151
/// <summary>
5252
/// The Movie Endpoint to put into the controller context.
5353
/// </summary>
54-
private const string MovieEndpoint = "http://localhost/tables/movies";
54+
protected const string MovieEndpoint = "http://localhost/tables/movies";
5555

5656
/// <summary>
5757
/// The number of movies in the dataset.
@@ -80,21 +80,6 @@ protected virtual async Task CreateControllerAsync(HttpMethod method = null, str
8080
this.tableController.ControllerContext.HttpContext = CreateHttpContext(method ?? HttpMethod.Get, uri);
8181
}
8282

83-
private async Task<List<TEntity>> GetListOfEntitiesAsync(IEnumerable<string> ids)
84-
{
85-
List<TEntity> entities = [];
86-
foreach (string id in ids)
87-
{
88-
TEntity entity = await GetEntityAsync(id);
89-
if (entity != null)
90-
{
91-
entities.Add(entity);
92-
}
93-
}
94-
95-
return entities;
96-
}
97-
9883
/// <summary>
9984
/// This is the base test for the individual query tests.
10085
/// </summary>

tests/CommunityToolkit.Datasync.Server.Test/Live/Cosmos_Controller_Tests.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
using CommunityToolkit.Datasync.Server.EntityFrameworkCore;
66
using CommunityToolkit.Datasync.Server.Test.Helpers;
7+
using CommunityToolkit.Datasync.TestCommon;
78
using CommunityToolkit.Datasync.TestCommon.Databases;
89
using Microsoft.EntityFrameworkCore;
10+
using System;
911
using Xunit.Abstractions;
1012

1113
namespace CommunityToolkit.Datasync.Server.Test.Live;
@@ -59,4 +61,22 @@ protected override Task<IRepository<CosmosEntityMovie>> GetPopulatedRepositoryAs
5961
protected override Task<string> GetRandomEntityIdAsync(bool exists)
6062
=> Task.FromResult(exists ? this.movies[this.random.Next(this.movies.Count)].Id : Guid.NewGuid().ToString());
6163
#endregion
64+
65+
/// <summary>
66+
/// We test the 400 Bad Request client-side evaluation error here because Cosmos has more restrictions than most,
67+
/// so it's easier to test the code path.
68+
/// </summary>
69+
[Fact]
70+
public async Task ClientSideEvaluation_Produces_400BadRequest()
71+
{
72+
Skip.IfNot(CanRunLiveTests());
73+
74+
IRepository<CosmosEntityMovie> repository = await GetPopulatedRepositoryAsync();
75+
TableController<CosmosEntityMovie> tableController = new(repository);
76+
tableController.ControllerContext.HttpContext = CreateHttpContext(HttpMethod.Get, $"{MovieEndpoint}?$filter=((year div 1000.5) eq 2)");
77+
78+
Func<Task> act = async () => _ = await tableController.QueryAsync();
79+
80+
(await act.Should().ThrowAsync<HttpException>()).WithStatusCode(400);
81+
}
6282
}

0 commit comments

Comments
 (0)