1616using Microsoft . OData ;
1717using System . Diagnostics . CodeAnalysis ;
1818using CommunityToolkit . Datasync . Server . OData ;
19- using Microsoft . AspNetCore . Http . Extensions ;
2019
2120namespace CommunityToolkit . Datasync . Server ;
2221
@@ -38,6 +37,9 @@ public partial class TableController<TEntity> : ODataController where TEntity :
3837 /// - <c>$skip</c> is used to skip some entities
3938 /// - <c>$top</c> is used to limit the number of entities returned.
4039 /// </para>
40+ /// <para>
41+ /// In addition, the <c>__includeDeleted</c> parameter is used to decide whether to include soft-deleted items in the result.
42+ /// </para>
4143 /// </summary>
4244 /// <param name="cancellationToken">A cancellation token</param>
4345 /// <returns>An <see cref="OkObjectResult"/> response object with the items.</returns>
@@ -50,10 +52,6 @@ public virtual async Task<IActionResult> QueryAsync(CancellationToken cancellati
5052 await AuthorizeRequestAsync ( TableOperation . Query , null , cancellationToken ) . ConfigureAwait ( false ) ;
5153 _ = BuildServiceProvider ( Request ) ;
5254
53- IQueryable < TEntity > dataset = ( await Repository . AsQueryableAsync ( cancellationToken ) . ConfigureAwait ( false ) )
54- . ApplyDataView ( AccessControlProvider . GetDataView ( ) )
55- . ApplyDeletedView ( Request , Options . EnableSoftDelete ) ;
56-
5755 ODataValidationSettings validationSettings = new ( ) { MaxTop = Options . MaxTop } ;
5856 ODataQuerySettings querySettings = new ( ) { PageSize = Options . PageSize , EnsureStableOrdering = true } ;
5957 ODataQueryContext queryContext = new ( EdmModel , typeof ( TEntity ) , new ODataPath ( ) ) ;
@@ -69,26 +67,28 @@ public virtual async Task<IActionResult> QueryAsync(CancellationToken cancellati
6967 return BadRequest ( validationException . Message ) ;
7068 }
7169
72- // Note that some IQueryable providers cannot execute all queries against the data source, so we have
73- // to switch to in-memory processing for those queries. This is done by calling ToListAsync() on the
74- // IQueryable. This is not ideal, but it is the only way to support all of the OData query options.
75- IEnumerable < object > ? results = null ;
76- await ExecuteQueryWithClientEvaluationAsync ( dataset , ds =>
77- {
78- results = ( IEnumerable < object > ) queryOptions . ApplyTo ( ds , querySettings ) ;
79- return Task . CompletedTask ;
80- } ) ;
70+ // Determine the dataset to be queried for this user.
71+ IQueryable < TEntity > dataset = ( await Repository . AsQueryableAsync ( cancellationToken ) . ConfigureAwait ( false ) )
72+ . ApplyDataView ( AccessControlProvider . GetDataView ( ) )
73+ . ApplyDeletedView ( Request , Options . EnableSoftDelete ) ;
8174
82- int count = 0 ;
83- FilterQueryOption ? filter = queryOptions . Filter ;
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- } ) ;
75+ // Apply the requested filter from the OData transaction.
76+ IQueryable < TEntity > filteredDataset = dataset . ApplyODataFilter ( queryOptions . Filter , querySettings ) ;
77+
78+ // 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 ) ;
80+
81+ // Now apply the OrderBy, Skip, and Top options to the dataset.
82+ IQueryable < TEntity > orderedDataset = filteredDataset
83+ . ApplyODataOrderBy ( queryOptions . OrderBy , querySettings )
84+ . ApplyODataPaging ( queryOptions , querySettings ) ;
85+
86+ // Get the list of items within the dataset that need to be returned.
87+ IList < TEntity > entitiesInResultSet = await Repository . ToListAsync ( orderedDataset , cancellationToken ) . ConfigureAwait ( false ) ;
8988
90- PagedResult result = BuildPagedResult ( queryOptions , results , count ) ;
91- Logger . LogInformation ( "Query: {Count} items being returned" , result . Items . Count ( ) ) ;
89+ // Produce the paged result.
90+ PagedResult result = BuildPagedResult ( queryOptions , entitiesInResultSet . ApplyODataSelect ( queryOptions . SelectExpand , querySettings ) , filteredCount ) ;
91+ Logger . LogInformation ( "Query: {Count} items being returned" , entitiesInResultSet . Count ) ;
9292 return Ok ( result ) ;
9393 }
9494
@@ -135,136 +135,39 @@ internal PagedResult BuildPagedResult(ODataQueryOptions queryOptions, IEnumerabl
135135 int resultCount = results ? . Count ( ) ?? 0 ;
136136 int skip = ( queryOptions . Skip ? . Value ?? 0 ) + resultCount ;
137137 int top = ( queryOptions . Top ? . Value ?? 0 ) - resultCount ;
138- if ( results is IEnumerable < ISelectExpandWrapper > wrapper )
139- {
140- results = wrapper . Select ( x => x . ToDictionary ( ) ) ;
141- }
142138
143- PagedResult result = new ( results ?? [ ] ) { Count = queryOptions . Count != null ? count : null } ;
144- if ( queryOptions . Top != null )
145- {
146- result . NextLink = skip >= count || top <= 0 ? null : CreateNextLink ( Request , skip , top ) ;
147- }
148- else
139+ // Internal function to create the nextLink property for the paged result.
140+ static string CreateNextLink ( HttpRequest request , int skip = 0 , int top = 0 )
149141 {
150- result . NextLink = skip >= count ? null : CreateNextLink ( Request , skip , 0 ) ;
151- }
142+ string ? queryString = request . QueryString . Value ;
143+ List < string > query = ( queryString ?? "" ) . TrimStart ( '?' )
144+ . Split ( '&' )
145+ . Where ( q => ! q . StartsWith ( $ "{ SkipParameterName } =") && ! q . StartsWith ( $ "{ TopParameterName } =") )
146+ . ToList ( ) ;
152147
153- return result ;
154- }
155-
156- /// <summary>
157- /// Given a very specific URI, creates a new query string with the same query, but with a different value for the <c>$skip</c> parameter.
158- /// </summary>
159- /// <param name="request">The original request.</param>
160- /// <param name="skip">The new skip value.</param>
161- /// <param name="top">The new top value.</param>
162- /// <returns>The new URI for the next page of items.</returns>
163- [ NonAction ]
164- [ SuppressMessage ( "Roslynator" , "RCS1158:Static member in generic type should use a type parameter" , Justification = "Static method in generic non-static class" ) ]
165- internal static string CreateNextLink ( HttpRequest request , int skip = 0 , int top = 0 )
166- => CreateNextLink ( new UriBuilder ( request . GetDisplayUrl ( ) ) . Query , skip , top ) ;
167-
168- /// <summary>
169- /// Given a very specific query string, creates a new query string with the same query, but with a different value for the <c>$skip</c> parameter.
170- /// </summary>
171- /// <param name="queryString">The original query string.</param>
172- /// <param name="skip">The new skip value.</param>
173- /// <param name="top">The new top value.</param>
174- /// <returns>The new URI for the next page of items.</returns>
175- [ NonAction ]
176- [ SuppressMessage ( "Roslynator" , "RCS1158:Static member in generic type should use a type parameter" , Justification = "Static method in generic non-static class" ) ]
177- internal static string CreateNextLink ( string queryString , int skip = 0 , int top = 0 )
178- {
179- List < string > query = ( queryString ?? "" ) . TrimStart ( '?' )
180- . Split ( '&' )
181- . Where ( q => ! q . StartsWith ( $ "{ SkipParameterName } =") && ! q . StartsWith ( $ "{ TopParameterName } =") )
182- . ToList ( ) ;
183-
184- if ( skip > 0 )
185- {
186- query . Add ( $ "{ SkipParameterName } ={ skip } ") ;
187- }
188-
189- if ( top > 0 )
190- {
191- query . Add ( $ "{ TopParameterName } ={ top } ") ;
192- }
193-
194- return string . Join ( '&' , query ) . TrimStart ( '&' ) ;
195- }
196-
197- /// <summary>
198- /// When doing a query evaluation, certain providers (e.g. Entity Framework) require some things
199- /// to be done client side. We use a client side evaluator to handle this case when it happens.
200- /// </summary>
201- /// <param name="ex">The exception thrown by the service-side evaluator</param>
202- /// <param name="reason">The reason if the client-side evaluator throws.</param>
203- /// <param name="clientSideEvaluator">The client-side evaluator</param>
204- [ NonAction ]
205- internal async Task CatchClientSideEvaluationExceptionAsync ( Exception ex , string reason , Func < Task > clientSideEvaluator )
206- {
207- if ( IsClientSideEvaluationException ( ex ) || IsClientSideEvaluationException ( ex . InnerException ) )
208- {
209- try
148+ if ( skip > 0 )
210149 {
211- await clientSideEvaluator . Invoke ( ) ;
150+ query . Add ( $ " { SkipParameterName } = { skip } " ) ;
212151 }
213- catch ( Exception err )
152+
153+ if ( top > 0 )
214154 {
215- Logger . LogError ( "Error while {reason}: {Message}" , reason , err . Message ) ;
216- throw ;
155+ query . Add ( $ "{ TopParameterName } ={ top } ") ;
217156 }
157+
158+ return string . Join ( '&' , query ) . TrimStart ( '&' ) ;
218159 }
219- else
220- {
221- throw ex ;
222- }
223- }
224160
225- /// <summary>
226- /// Executes an evaluation of a query, using a client-side evaluation if necessary.
227- /// </summary>
228- /// <param name="dataset">The dataset to be evaluated.</param>
229- /// <param name="evaluator">The base evaluation to be performed.</param>
230- [ NonAction ]
231- internal async Task ExecuteQueryWithClientEvaluationAsync ( IQueryable < TEntity > dataset , Func < IQueryable < TEntity > , Task > evaluator )
232- {
233- try
161+ PagedResult result = new ( results ?? [ ] ) { Count = queryOptions . Count != null ? count : null } ;
162+ if ( queryOptions . Top is not null )
234163 {
235- await evaluator . Invoke ( dataset ) ;
164+ result . NextLink = skip >= count || top <= 0 ? null : CreateNextLink ( Request , skip , top ) ;
236165 }
237- catch ( Exception ex ) when ( ! Options . DisableClientSideEvaluation )
166+ else
238167 {
239- await CatchClientSideEvaluationExceptionAsync ( ex , "executing query" , async ( ) =>
240- {
241- Logger . LogWarning ( "Error while executing query: possible client-side evaluation ({Message})" , ex . InnerException ? . Message ?? ex . Message ) ;
242- await evaluator . Invoke ( dataset . ToList ( ) . AsQueryable ( ) ) ;
243- } ) ;
168+ result . NextLink = skip >= count ? null : CreateNextLink ( Request , skip , 0 ) ;
244169 }
245- }
246170
247- /// <summary>
248- /// Determines if a particular exception indicates a client-side evaluation is required.
249- /// </summary>
250- /// <param name="ex">The exception that was thrown by the service-side evaluator</param>
251- /// <returns>true if a client-side evaluation is required.</returns>
252- [ NonAction ]
253- [ SuppressMessage ( "Roslynator" , "RCS1158:Static member in generic type should use a type parameter." ) ]
254- internal static bool IsClientSideEvaluationException ( Exception ? ex )
255- => 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 ) ;
171+ return result ;
269172 }
270173}
0 commit comments