diff --git a/src/CommunityToolkit.Datasync.Server/Controllers/TableController.Query.cs b/src/CommunityToolkit.Datasync.Server/Controllers/TableController.Query.cs index af65def..5e6d75b 100644 --- a/src/CommunityToolkit.Datasync.Server/Controllers/TableController.Query.cs +++ b/src/CommunityToolkit.Datasync.Server/Controllers/TableController.Query.cs @@ -73,11 +73,19 @@ public virtual async Task QueryAsync(CancellationToken cancellati // to switch to in-memory processing for those queries. This is done by calling ToListAsync() on the // IQueryable. This is not ideal, but it is the only way to support all of the OData query options. IEnumerable? results = null; - ExecuteQueryWithClientEvaluation(dataset, ds => results = (IEnumerable)queryOptions.ApplyTo(ds, querySettings)); + await ExecuteQueryWithClientEvaluationAsync(dataset, ds => + { + results = (IEnumerable)queryOptions.ApplyTo(ds, querySettings); + return Task.CompletedTask; + }); int count = 0; FilterQueryOption? filter = queryOptions.Filter; - ExecuteQueryWithClientEvaluation(dataset, ds => { IQueryable q = (IQueryable)(filter?.ApplyTo(ds, new ODataQuerySettings()) ?? ds); count = q.Count(); }); + await ExecuteQueryWithClientEvaluationAsync(dataset, async ds => + { + IQueryable q = (IQueryable)(filter?.ApplyTo(ds, new ODataQuerySettings()) ?? ds); + count = await CountAsync(q, cancellationToken); + }); PagedResult result = BuildPagedResult(queryOptions, results, count); 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 /// The reason if the client-side evaluator throws. /// The client-side evaluator [NonAction] - internal void CatchClientSideEvaluationException(Exception ex, string reason, Action clientSideEvaluator) + internal async Task CatchClientSideEvaluationExceptionAsync(Exception ex, string reason, Func clientSideEvaluator) { if (IsClientSideEvaluationException(ex) || IsClientSideEvaluationException(ex.InnerException)) { try { - clientSideEvaluator.Invoke(); + await clientSideEvaluator.Invoke(); } catch (Exception err) { @@ -220,18 +228,18 @@ internal void CatchClientSideEvaluationException(Exception ex, string reason, Ac /// The dataset to be evaluated. /// The base evaluation to be performed. [NonAction] - internal void ExecuteQueryWithClientEvaluation(IQueryable dataset, Action> evaluator) + internal async Task ExecuteQueryWithClientEvaluationAsync(IQueryable dataset, Func, Task> evaluator) { try { - evaluator.Invoke(dataset); + await evaluator.Invoke(dataset); } catch (Exception ex) when (!Options.DisableClientSideEvaluation) { - CatchClientSideEvaluationException(ex, "executing query", () => + await CatchClientSideEvaluationExceptionAsync(ex, "executing query", async () => { Logger.LogWarning("Error while executing query: possible client-side evaluation ({Message})", ex.InnerException?.Message ?? ex.Message); - evaluator.Invoke(dataset.ToList().AsQueryable()); + await evaluator.Invoke(dataset.ToList().AsQueryable()); }); } } @@ -245,4 +253,18 @@ internal void ExecuteQueryWithClientEvaluation(IQueryable dataset, Acti [SuppressMessage("Roslynator", "RCS1158:Static member in generic type should use a type parameter.")] internal static bool IsClientSideEvaluationException(Exception? ex) => ex is not null and (InvalidOperationException or NotSupportedException); + + /// + /// This is an overridable method that calls Count() on the provided queryable. You can override + /// this to calls a provider-specific count mechanism (e.g. CountAsync(). + /// + /// + /// + /// + [NonAction] + public virtual Task CountAsync(IQueryable query, CancellationToken cancellationToken) + { + int result = query.Count(); + return Task.FromResult(result); + } } diff --git a/tests/CommunityToolkit.Datasync.Server.Test/Controllers/TableController_Query_Tests.cs b/tests/CommunityToolkit.Datasync.Server.Test/Controllers/TableController_Query_Tests.cs index ab1770f..5fe84c6 100644 --- a/tests/CommunityToolkit.Datasync.Server.Test/Controllers/TableController_Query_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Server.Test/Controllers/TableController_Query_Tests.cs @@ -41,167 +41,173 @@ public void BuildPagedResult_NulLArg_BuildsPagedResult() #region CatchClientSideEvaluationException [Fact] - public void CatchClientSideEvaluationException_NotCCEE_ThrowsOriginalException() + public async Task CatchClientSideEvaluationException_NotCCEE_ThrowsOriginalException() { TableController controller = new() { Repository = new InMemoryRepository() }; ApplicationException exception = new("Original exception"); - static void evaluator() { throw new ApplicationException("In evaluator"); } + static Task evaluator() { throw new ApplicationException("In evaluator"); } - Action act = () => controller.CatchClientSideEvaluationException(exception, "foo", evaluator); - act.Should().Throw().WithMessage("Original exception"); + Func act = async () => await controller.CatchClientSideEvaluationExceptionAsync(exception, "foo", evaluator); + (await act.Should().ThrowAsync()).WithMessage("Original exception"); } [Fact] - public void CatchClientSideEvaluationException_NotCCEE_WithInner_ThrowsOriginalException() + public async Task CatchClientSideEvaluationException_NotCCEE_WithInner_ThrowsOriginalException() { TableController controller = new() { Repository = new InMemoryRepository() }; ApplicationException exception = new("Original exception", new ApplicationException()); - static void evaluator() { throw new ApplicationException("In evaluator"); } + static Task evaluator() { throw new ApplicationException("In evaluator"); } - Action act = () => controller.CatchClientSideEvaluationException(exception, "foo", evaluator); - act.Should().Throw().WithMessage("Original exception"); + Func act = async () => await controller.CatchClientSideEvaluationExceptionAsync(exception, "foo", evaluator); + (await act.Should().ThrowAsync()).WithMessage("Original exception"); } [Fact] - public void CatchClientSideEvaluationException_CCEE_ThrowsEvaluatorException() + public async Task CatchClientSideEvaluationException_CCEE_ThrowsEvaluatorException() { TableController controller = new() { Repository = new InMemoryRepository() }; NotSupportedException exception = new("Original exception", new ApplicationException("foo")); - static void evaluator() { throw new ApplicationException("In evaluator"); } + static Task evaluator() { throw new ApplicationException("In evaluator"); } - Action act = () => controller.CatchClientSideEvaluationException(exception, "foo", evaluator); - act.Should().Throw().WithMessage("In evaluator"); + Func act = async () => await controller.CatchClientSideEvaluationExceptionAsync(exception, "foo", evaluator); + (await act.Should().ThrowAsync()).WithMessage("In evaluator"); } [Fact] - public void CatchClientSideEvaluationException_CCEEInner_ThrowsEvaluatorException() + public async Task CatchClientSideEvaluationException_CCEEInner_ThrowsEvaluatorException() { TableController controller = new() { Repository = new InMemoryRepository() }; ApplicationException exception = new("Original exception", new NotSupportedException("foo")); - static void evaluator() { throw new ApplicationException("In evaluator"); } + static Task evaluator() { throw new ApplicationException("In evaluator"); } - Action act = () => controller.CatchClientSideEvaluationException(exception, "foo", evaluator); - act.Should().Throw().WithMessage("In evaluator"); + Func act = async () => await controller.CatchClientSideEvaluationExceptionAsync(exception, "foo", evaluator); + (await act.Should().ThrowAsync()).WithMessage("In evaluator"); } [Fact] - public void CatchClientSideEvaluationException_CCEE_ExecutesEvaluator() + public async Task CatchClientSideEvaluationException_CCEE_ExecutesEvaluator() { bool isExecuted = false; TableController controller = new() { Repository = new InMemoryRepository() }; NotSupportedException exception = new("Original exception", new ApplicationException("foo")); - Action act = () => controller.CatchClientSideEvaluationException(exception, "foo", () => isExecuted = true); - act.Should().NotThrow(); + + Func act = async () => await controller.CatchClientSideEvaluationExceptionAsync(exception, "foo", () => { isExecuted = true; return Task.CompletedTask; }); + await act.Should().NotThrowAsync(); isExecuted.Should().BeTrue(); } [Fact] - public void CatchClientSideEvaluationException_CCEEInner_ExecutesEvaluator() + public async Task CatchClientSideEvaluationException_CCEEInner_ExecutesEvaluator() { bool isExecuted = false; TableController controller = new() { Repository = new InMemoryRepository() }; ApplicationException exception = new("Original exception", new NotSupportedException("foo")); - Action act = () => controller.CatchClientSideEvaluationException(exception, "foo", () => isExecuted = true); - act.Should().NotThrow(); + + Func act = async () => await controller.CatchClientSideEvaluationExceptionAsync(exception, "foo", () => { isExecuted = true; return Task.CompletedTask; }); + await act.Should().NotThrowAsync(); isExecuted.Should().BeTrue(); } #endregion #region ExecuteQueryWithClientEvaluation [Fact] - public void ExecuteQueryWithClientEvaluation_ExecutesServiceSide() + public async Task ExecuteQueryWithClientEvaluation_ExecutesServiceSide() { TableController controller = new() { Repository = new InMemoryRepository() }; controller.Options.DisableClientSideEvaluation = true; int evaluations = 0; - void evaluator(IQueryable dataset) + Task evaluator(IQueryable dataset) { evaluations++; // if (evaluations == 1) throw new NotSupportedException("Server side"); // if (evaluations == 2) throw new NotSupportedException("Client side"); + return Task.CompletedTask; } List dataset = []; - Action act = () => controller.ExecuteQueryWithClientEvaluation(dataset.AsQueryable(), evaluator); + Func act = async () => await controller.ExecuteQueryWithClientEvaluationAsync(dataset.AsQueryable(), evaluator); - act.Should().NotThrow(); + await act.Should().NotThrowAsync(); evaluations.Should().Be(1); } [Fact] - public void ExecuteQueryWithClientEvaluation_ThrowsServiceSide_WhenClientEvaluationDisabled() + public async Task ExecuteQueryWithClientEvaluation_ThrowsServiceSide_WhenClientEvaluationDisabled() { TableController controller = new() { Repository = new InMemoryRepository() }; controller.Options.DisableClientSideEvaluation = true; int evaluations = 0; #pragma warning disable IDE0011 // Add braces - void evaluator(IQueryable dataset) + Task evaluator(IQueryable dataset) { evaluations++; if (evaluations == 1) throw new NotSupportedException("Server side"); if (evaluations == 2) throw new NotSupportedException("Client side"); + return Task.CompletedTask; } #pragma warning restore IDE0011 // Add braces List dataset = []; - Action act = () => controller.ExecuteQueryWithClientEvaluation(dataset.AsQueryable(), evaluator); + Func act = async () => await controller.ExecuteQueryWithClientEvaluationAsync(dataset.AsQueryable(), evaluator); - act.Should().Throw().WithMessage("Server side"); + (await act.Should().ThrowAsync()).WithMessage("Server side"); } [Fact] - public void ExecuteQueryWithClientEvaluation_ExecutesClientSide_WhenClientEvaluationEnabled() + public async Task ExecuteQueryWithClientEvaluation_ExecutesClientSide_WhenClientEvaluationEnabled() { TableController controller = new() { Repository = new InMemoryRepository() }; controller.Options.DisableClientSideEvaluation = false; int evaluations = 0; #pragma warning disable IDE0011 // Add braces - void evaluator(IQueryable dataset) + Task evaluator(IQueryable dataset) { evaluations++; if (evaluations == 1) throw new NotSupportedException("Server side"); //if (evaluations == 2) throw new NotSupportedException("Client side"); + return Task.CompletedTask; } #pragma warning restore IDE0011 // Add braces List dataset = []; - Action act = () => controller.ExecuteQueryWithClientEvaluation(dataset.AsQueryable(), evaluator); + Func act = async () => await controller.ExecuteQueryWithClientEvaluationAsync(dataset.AsQueryable(), evaluator); - act.Should().NotThrow(); + await act.Should().NotThrowAsync(); evaluations.Should().Be(2); } [Fact] - public void ExecuteQueryWithClientEvaluation_ThrowsClientSide_WhenClientEvaluationEnabled() + public async Task ExecuteQueryWithClientEvaluation_ThrowsClientSide_WhenClientEvaluationEnabled() { TableController controller = new() { Repository = new InMemoryRepository() }; controller.Options.DisableClientSideEvaluation = false; int evaluations = 0; #pragma warning disable IDE0011 // Add braces - void evaluator(IQueryable dataset) + Task evaluator(IQueryable dataset) { evaluations++; if (evaluations == 1) throw new NotSupportedException("Server side", new ApplicationException("Inner exception")); if (evaluations == 2) throw new NotSupportedException("Client side"); + return Task.CompletedTask; } #pragma warning restore IDE0011 // Add braces List dataset = []; - Action act = () => controller.ExecuteQueryWithClientEvaluation(dataset.AsQueryable(), evaluator); + Func act = async () => await controller.ExecuteQueryWithClientEvaluationAsync(dataset.AsQueryable(), evaluator); - act.Should().Throw().WithMessage("Client side"); + (await act.Should().ThrowAsync()).WithMessage("Client side"); evaluations.Should().Be(2); } #endregion