Skip to content

Commit

Permalink
(#184) Allow query calls to be async (#191)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianhall authored Jan 10, 2025
1 parent cd61a24 commit 09ce5e8
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,19 @@ public virtual async Task<IActionResult> 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<object>? results = null;
ExecuteQueryWithClientEvaluation(dataset, ds => results = (IEnumerable<object>)queryOptions.ApplyTo(ds, querySettings));
await ExecuteQueryWithClientEvaluationAsync(dataset, ds =>
{
results = (IEnumerable<object>)queryOptions.ApplyTo(ds, querySettings);
return Task.CompletedTask;
});

int count = 0;
FilterQueryOption? filter = queryOptions.Filter;
ExecuteQueryWithClientEvaluation(dataset, ds => { IQueryable<TEntity> q = (IQueryable<TEntity>)(filter?.ApplyTo(ds, new ODataQuerySettings()) ?? ds); count = q.Count(); });
await ExecuteQueryWithClientEvaluationAsync(dataset, async ds =>
{
IQueryable<TEntity> q = (IQueryable<TEntity>)(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());
Expand Down Expand Up @@ -194,13 +202,13 @@ internal static string CreateNextLink(string queryString, int skip = 0, int top
/// <param name="reason">The reason if the client-side evaluator throws.</param>
/// <param name="clientSideEvaluator">The client-side evaluator</param>
[NonAction]
internal void CatchClientSideEvaluationException(Exception ex, string reason, Action clientSideEvaluator)
internal async Task CatchClientSideEvaluationExceptionAsync(Exception ex, string reason, Func<Task> clientSideEvaluator)
{
if (IsClientSideEvaluationException(ex) || IsClientSideEvaluationException(ex.InnerException))
{
try
{
clientSideEvaluator.Invoke();
await clientSideEvaluator.Invoke();
}
catch (Exception err)
{
Expand All @@ -220,18 +228,18 @@ internal void CatchClientSideEvaluationException(Exception ex, string reason, Ac
/// <param name="dataset">The dataset to be evaluated.</param>
/// <param name="evaluator">The base evaluation to be performed.</param>
[NonAction]
internal void ExecuteQueryWithClientEvaluation(IQueryable<TEntity> dataset, Action<IQueryable<TEntity>> evaluator)
internal async Task ExecuteQueryWithClientEvaluationAsync(IQueryable<TEntity> dataset, Func<IQueryable<TEntity>, 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());
});
}
}
Expand All @@ -245,4 +253,18 @@ internal void ExecuteQueryWithClientEvaluation(IQueryable<TEntity> 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);

/// <summary>
/// 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().
/// </summary>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[NonAction]
public virtual Task<int> CountAsync(IQueryable<TEntity> query, CancellationToken cancellationToken)
{
int result = query.Count();
return Task.FromResult(result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,167 +41,173 @@ public void BuildPagedResult_NulLArg_BuildsPagedResult()

#region CatchClientSideEvaluationException
[Fact]
public void CatchClientSideEvaluationException_NotCCEE_ThrowsOriginalException()
public async Task CatchClientSideEvaluationException_NotCCEE_ThrowsOriginalException()
{
TableController<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
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<ApplicationException>().WithMessage("Original exception");
Func<Task> act = async () => await controller.CatchClientSideEvaluationExceptionAsync(exception, "foo", evaluator);
(await act.Should().ThrowAsync<ApplicationException>()).WithMessage("Original exception");
}

[Fact]
public void CatchClientSideEvaluationException_NotCCEE_WithInner_ThrowsOriginalException()
public async Task CatchClientSideEvaluationException_NotCCEE_WithInner_ThrowsOriginalException()
{
TableController<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
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<ApplicationException>().WithMessage("Original exception");
Func<Task> act = async () => await controller.CatchClientSideEvaluationExceptionAsync(exception, "foo", evaluator);
(await act.Should().ThrowAsync<ApplicationException>()).WithMessage("Original exception");
}

[Fact]
public void CatchClientSideEvaluationException_CCEE_ThrowsEvaluatorException()
public async Task CatchClientSideEvaluationException_CCEE_ThrowsEvaluatorException()
{
TableController<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
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<ApplicationException>().WithMessage("In evaluator");
Func<Task> act = async () => await controller.CatchClientSideEvaluationExceptionAsync(exception, "foo", evaluator);
(await act.Should().ThrowAsync<ApplicationException>()).WithMessage("In evaluator");
}

[Fact]
public void CatchClientSideEvaluationException_CCEEInner_ThrowsEvaluatorException()
public async Task CatchClientSideEvaluationException_CCEEInner_ThrowsEvaluatorException()
{
TableController<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
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<ApplicationException>().WithMessage("In evaluator");
Func<Task> act = async () => await controller.CatchClientSideEvaluationExceptionAsync(exception, "foo", evaluator);
(await act.Should().ThrowAsync<ApplicationException>()).WithMessage("In evaluator");
}

[Fact]
public void CatchClientSideEvaluationException_CCEE_ExecutesEvaluator()
public async Task CatchClientSideEvaluationException_CCEE_ExecutesEvaluator()
{
bool isExecuted = false;
TableController<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
NotSupportedException exception = new("Original exception", new ApplicationException("foo"));
Action act = () => controller.CatchClientSideEvaluationException(exception, "foo", () => isExecuted = true);
act.Should().NotThrow();

Func<Task> 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<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
ApplicationException exception = new("Original exception", new NotSupportedException("foo"));
Action act = () => controller.CatchClientSideEvaluationException(exception, "foo", () => isExecuted = true);
act.Should().NotThrow();

Func<Task> 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<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
controller.Options.DisableClientSideEvaluation = true;

int evaluations = 0;
void evaluator(IQueryable<InMemoryMovie> dataset)
Task evaluator(IQueryable<InMemoryMovie> dataset)
{
evaluations++;
// if (evaluations == 1) throw new NotSupportedException("Server side");
// if (evaluations == 2) throw new NotSupportedException("Client side");
return Task.CompletedTask;
}

List<InMemoryMovie> dataset = [];

Action act = () => controller.ExecuteQueryWithClientEvaluation(dataset.AsQueryable(), evaluator);
Func<Task> 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<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
controller.Options.DisableClientSideEvaluation = true;

int evaluations = 0;
#pragma warning disable IDE0011 // Add braces
void evaluator(IQueryable<InMemoryMovie> dataset)
Task evaluator(IQueryable<InMemoryMovie> 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<InMemoryMovie> dataset = [];

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

act.Should().Throw<NotSupportedException>().WithMessage("Server side");
(await act.Should().ThrowAsync<NotSupportedException>()).WithMessage("Server side");
}

[Fact]
public void ExecuteQueryWithClientEvaluation_ExecutesClientSide_WhenClientEvaluationEnabled()
public async Task ExecuteQueryWithClientEvaluation_ExecutesClientSide_WhenClientEvaluationEnabled()
{
TableController<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
controller.Options.DisableClientSideEvaluation = false;

int evaluations = 0;
#pragma warning disable IDE0011 // Add braces
void evaluator(IQueryable<InMemoryMovie> dataset)
Task evaluator(IQueryable<InMemoryMovie> 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<InMemoryMovie> dataset = [];

Action act = () => controller.ExecuteQueryWithClientEvaluation(dataset.AsQueryable(), evaluator);
Func<Task> 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<InMemoryMovie> controller = new() { Repository = new InMemoryRepository<InMemoryMovie>() };
controller.Options.DisableClientSideEvaluation = false;

int evaluations = 0;
#pragma warning disable IDE0011 // Add braces
void evaluator(IQueryable<InMemoryMovie> dataset)
Task evaluator(IQueryable<InMemoryMovie> 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<InMemoryMovie> dataset = [];

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

act.Should().Throw<NotSupportedException>().WithMessage("Client side");
(await act.Should().ThrowAsync<NotSupportedException>()).WithMessage("Client side");
evaluations.Should().Be(2);
}
#endregion
Expand Down

0 comments on commit 09ce5e8

Please sign in to comment.