Skip to content

Commit 1171290

Browse files
committed
disable LoadFromQueryableAsync on .NET Framework, add some doccomments
1 parent 0821f68 commit 1171290

File tree

5 files changed

+59
-30
lines changed

5 files changed

+59
-30
lines changed

src/Framework/Core/Controls/GridViewDataSetExtensions.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ namespace DotVVM.Framework.Controls
88
{
99
public static class GridViewDataSetExtensions
1010
{
11-
11+
/// <summary>
12+
/// Loads dataset from the specified <paramref name="queryable" />: Applies filtering, sorting and paging options, and collects the results. If <see cref="PagingOptions"/> is used, the total number of items (after applying filtering) is retrieved and stored in the PagingOptions property.
13+
/// </summary>
1214
public static void LoadFromQueryable<T>(this IGridViewDataSet<T> dataSet, IQueryable<T> queryable)
1315
{
1416
if (dataSet.FilteringOptions is not IApplyToQueryable filteringOptions)
@@ -37,6 +39,11 @@ public static void LoadFromQueryable<T>(this IGridViewDataSet<T> dataSet, IQuery
3739
dataSet.IsRefreshRequired = false;
3840
}
3941

42+
#if NET6_0_OR_GREATER
43+
/// <summary>
44+
/// Loads dataset from the specified <paramref name="queryable" />: Applies filtering, sorting and paging options, and collects the results using IAsyncEnumerable. If <see cref="PagingOptions"/> is used, the total number of items (after applying filtering) is retrieved and stored in the PagingOptions property.
45+
/// </summary>
46+
/// <exception cref="ArgumentException">The specified IQueryable does not support async enumeration.</exception>
4047
public static async Task LoadFromQueryableAsync<T>(this IGridViewDataSet<T> dataSet, IQueryable<T> queryable, CancellationToken cancellationToken = default)
4148
{
4249
if (dataSet.FilteringOptions is not IApplyToQueryable filteringOptions)
@@ -69,36 +76,48 @@ public static async Task LoadFromQueryableAsync<T>(this IGridViewDataSet<T> data
6976

7077
if (pagingOptions is IPagingOptionsLoadingPostProcessor pagingOptionsLoadingPostProcessor)
7178
{
79+
cancellationToken.ThrowIfCancellationRequested();
7280
await pagingOptionsLoadingPostProcessor.ProcessLoadedItemsAsync(filtered, result, cancellationToken);
7381
}
7482

7583
dataSet.IsRefreshRequired = false;
7684
}
85+
#endif
7786

87+
/// <summary> Sets <see cref="IPagingOptions"/> to the first page, and sets the <see cref="IRefreshableGridViewDataSet.IsRefreshRequired"/> property to true. </summary>
88+
/// <remarks> To reload the dataset, you must then call <see cref="LoadFromQueryable{T}(IGridViewDataSet{T}, IQueryable{T})"/> or a similar method. </remarks>
7889
public static void GoToFirstPageAndRefresh(this IPageableGridViewDataSet<IPagingFirstPageCapability> dataSet)
7990
{
8091
dataSet.PagingOptions.GoToFirstPage();
8192
(dataSet as IRefreshableGridViewDataSet)?.RequestRefresh();
8293
}
8394

95+
/// <summary> Sets <see cref="IPagingOptions"/> to the last page, and sets the <see cref="IRefreshableGridViewDataSet.IsRefreshRequired"/> property to true. </summary>
96+
/// <remarks> To reload the dataset, you must then call <see cref="LoadFromQueryable{T}(IGridViewDataSet{T}, IQueryable{T})"/> or a similar method. </remarks>
8497
public static void GoToLastPageAndRefresh(this IPageableGridViewDataSet<IPagingLastPageCapability> dataSet)
8598
{
8699
dataSet.PagingOptions.GoToLastPage();
87100
(dataSet as IRefreshableGridViewDataSet)?.RequestRefresh();
88101
}
89102

103+
/// <summary> Sets <see cref="IPagingOptions"/> to the previous page, and sets the <see cref="IRefreshableGridViewDataSet.IsRefreshRequired"/> property to true. </summary>
104+
/// <remarks> To reload the dataset, you must then call <see cref="LoadFromQueryable{T}(IGridViewDataSet{T}, IQueryable{T})"/> or a similar method. </remarks>
90105
public static void GoToPreviousPageAndRefresh(this IPageableGridViewDataSet<IPagingPreviousPageCapability> dataSet)
91106
{
92107
dataSet.PagingOptions.GoToPreviousPage();
93108
(dataSet as IRefreshableGridViewDataSet)?.RequestRefresh();
94109
}
95110

111+
/// <summary> Sets <see cref="IPagingOptions"/> to the next page, and sets the <see cref="IRefreshableGridViewDataSet.IsRefreshRequired"/> property to true. </summary>
112+
/// <remarks> To reload the dataset, you must then call <see cref="LoadFromQueryable{T}(IGridViewDataSet{T}, IQueryable{T})"/> or a similar method. </remarks>
96113
public static void GoToNextPageAndRefresh(this IPageableGridViewDataSet<IPagingNextPageCapability> dataSet)
97114
{
98115
dataSet.PagingOptions.GoToNextPage();
99116
(dataSet as IRefreshableGridViewDataSet)?.RequestRefresh();
100117
}
101118

119+
/// <summary> Sets <see cref="IPagingOptions"/> to the page number <paramref name="pageIndex"/> (indexed from 0), and sets the <see cref="IRefreshableGridViewDataSet.IsRefreshRequired"/> property to true. </summary>
120+
/// <remarks> To reload the dataset, you must then call <see cref="LoadFromQueryable{T}(IGridViewDataSet{T}, IQueryable{T})"/> or a similar method. </remarks>
102121
public static void GoToPageAndRefresh(this IPageableGridViewDataSet<IPagingPageIndexCapability> dataSet, int pageIndex)
103122
{
104123
dataSet.PagingOptions.GoToPage(pageIndex);

src/Framework/Core/Controls/GridViewDataSetResult.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace DotVVM.Framework.Controls
44
{
5+
56
public class GridViewDataSetResult<TItem, TFilteringOptions, TSortingOptions, TPagingOptions>
67
where TFilteringOptions : IFilteringOptions
78
where TSortingOptions : ISortingOptions
@@ -23,12 +24,16 @@ public GridViewDataSetResult(IReadOnlyList<TItem> items, GridViewDataSetOptions<
2324
PagingOptions = options.PagingOptions;
2425
}
2526

27+
/// <summary> New items to replace the old <see cref="IBaseGridViewDataSet{T}.Items"/> </summary>
2628
public IReadOnlyList<TItem> Items { get; }
2729

30+
/// <summary> New filtering options to replace the old <see cref="IFilterableGridViewDataSet{T}.FilteringOptions"/>, if null the old options are left unchanged. </summary>
2831
public TFilteringOptions? FilteringOptions { get; }
2932

33+
/// <summary> New sorting options to replace the old <see cref="ISortableGridViewDataSet{T}.SortingOptions"/>, if null the old options are left unchanged. </summary>
3034
public TSortingOptions? SortingOptions { get; }
3135

36+
/// <summary> New paging options to replace the old <see cref="IPageableGridViewDataSet{T}.PagingOptions"/>, if null the old options are left unchanged. </summary>
3237
public TPagingOptions? PagingOptions { get; }
3338
}
3439
}

src/Framework/Core/Controls/Options/PagingImplementation.cs

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public static IQueryable<T> ApplyPagingToQueryable<T, TPagingOptions>(IQueryable
2424
: queryable;
2525
}
2626

27-
/// <summary> Attempts to count the queryable asynchronously. EF Core IQueryables are supported, and IQueryables which return IAsyncEnumerable from GroupBy operator also work correctly. Otherwise, a synchronous fallback is user, or <see cref="CustomAsyncQueryableCountDelegate" /> may be set to add support for an ORM mapper of choice. </summary>
27+
#if NET6_0_OR_GREATER
28+
/// <summary> Attempts to count the queryable asynchronously. EF Core IQueryables are supported, and IQueryables which return IAsyncEnumerable from GroupBy operator also work correctly. Otherwise, a synchronous fallback is used, or <see cref="CustomAsyncQueryableCountDelegate" /> may be set to add support for an ORM mapper of choice. </summary>
2829
public static async Task<int> QueryableAsyncCount<T>(IQueryable<T> queryable, CancellationToken ct = default)
2930
{
3031
if (CustomAsyncQueryableCountDelegate is {} customDelegate)
@@ -37,9 +38,14 @@ public static async Task<int> QueryableAsyncCount<T>(IQueryable<T> queryable, Ca
3738
}
3839

3940
var queryableType = queryable.GetType();
41+
// Note: there is not a standard way to get a count from IAsyncEnumerable instance, without enumerating it.
42+
// we use two heuristics to try to get the count using the query provider:
43+
// * if we detect usage of EF Core, call its CountAsync method
44+
// * otherwise, do it as .GroupBy(_ => 1).Select(group => group.Count()).SingleOrDefault()
45+
// (if you are reading this and need a separate hack for your favorite ORM, you can set
46+
// CustomAsyncQueryableCountDelegate, and we do accept PRs adding new heuristics ;) )
4047
return await (
41-
EfCoreAsyncCountHack(queryable, queryableType, ct) ?? // TODO: test this
42-
Ef6AsyncCountHack(queryable, ct) ?? // TODO: test this
48+
EfCoreAsyncCountHack(queryable, queryableType, ct) ??
4349
StandardAsyncCountHack(queryable, ct)
4450
);
4551
}
@@ -64,34 +70,14 @@ public static async Task<int> QueryableAsyncCount<T>(IQueryable<T> queryable, Ca
6470
return (Task<int>)countMethodGeneric.Invoke(null, new object[] { queryable, ct })!;
6571
}
6672

67-
static readonly Type? ef6IDbAsyncQueryProvider = Type.GetType("System.Data.Entity.Infrastructure.IDbAsyncQueryProvider, EntityFramework"); // https://learn.microsoft.com/en-us/dotnet/api/system.data.entity.infrastructure.idbasyncqueryprovider?view=entity-framework-6.2.0
68-
static MethodInfo? ef6MethodCache;
69-
static Task<int>? Ef6AsyncCountHack<T>(IQueryable<T> queryable, CancellationToken ct)
70-
{
71-
if (ef6IDbAsyncQueryProvider is null)
72-
return null;
73-
if (!ef6IDbAsyncQueryProvider.IsInstanceOfType(queryable.Provider))
74-
return null;
75-
76-
var countMethod = ef6MethodCache ?? Type.GetType("System.Data.Entity.QueryableExtensions, EntityFramework")!.GetMethods().SingleOrDefault(m => m.Name == "CountAsync" && m.GetParameters() is { Length: 2 } parameters && parameters[1].ParameterType == typeof(CancellationToken))!;
77-
if (countMethod is null)
78-
return null;
79-
80-
if (ef6MethodCache is null)
81-
Interlocked.CompareExchange(ref ef6MethodCache, countMethod, null);
82-
83-
var countMethodGeneric = countMethod.MakeGenericMethod(typeof(T));
84-
return (Task<int>)countMethodGeneric.Invoke(null, new object[] { queryable, ct })!;
85-
}
86-
8773
static Task<int> StandardAsyncCountHack<T>(IQueryable<T> queryable, CancellationToken ct)
8874
{
8975
#if NETSTANDARD2_1_OR_GREATER
9076
var countGroupHack = queryable.GroupBy(_ => 1).Select(group => group.Count());
91-
// if not IAsyncEnumerable, just use synchronous Count
77+
// if not IAsyncEnumerable, just use synchronous Count on a new thread
9278
if (countGroupHack is not IAsyncEnumerable<int> countGroupEnumerable)
9379
{
94-
return Task.FromResult(queryable.Count());
80+
return Task.Factory.StartNew(() => queryable.Count(), TaskCreationOptions.LongRunning);
9581
}
9682

9783
return FirstOrDefaultAsync(countGroupEnumerable, ct);
@@ -105,5 +91,6 @@ static Task<int> StandardAsyncCountHack<T>(IQueryable<T> queryable, Cancellation
10591
throw new Exception("IAsyncEnumerable is not supported on .NET Framework and the queryable does not support EntityFramework CountAsync.");
10692
#endif
10793
}
94+
#endif
10895
}
10996
}

src/Framework/Core/Controls/Options/PagingOptions.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,29 @@ public virtual IQueryable<T> ApplyToQueryable<T>(IQueryable<T> queryable)
125125

126126
public virtual void ProcessLoadedItems<T>(IQueryable<T> filteredQueryable, IList<T> items)
127127
{
128-
TotalItemsCount = filteredQueryable.Count();
128+
if (items.Count < PageSize)
129+
{
130+
TotalItemsCount = PageIndex * PageSize + items.Count;
131+
}
132+
else
133+
{
134+
TotalItemsCount = filteredQueryable.Count();
135+
}
129136
}
130137
public async Task ProcessLoadedItemsAsync<T>(IQueryable<T> filteredQueryable, IList<T> items, CancellationToken cancellationToken)
131138
{
132-
TotalItemsCount = await PagingImplementation.QueryableAsyncCount(filteredQueryable, cancellationToken);
139+
#if NET6_0_OR_GREATER
140+
if (items.Count < PageSize)
141+
{
142+
TotalItemsCount = PageIndex * PageSize + items.Count;
143+
}
144+
else
145+
{
146+
TotalItemsCount = await PagingImplementation.QueryableAsyncCount(filteredQueryable, cancellationToken);
147+
}
148+
#else
149+
throw new NotSupportedException("LoadFromQueryableAsync and ProcessLoadedItemsAsync methods are not supported on .NET Framework.");
150+
#endif
133151
}
134152

135153
}

src/Tests/DotVVM.Framework.Tests.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@
4949
<ItemGroup>
5050
<PackageReference Include="AngleSharp" Version="0.17.0" />
5151
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
52-
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
53-
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
52+
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
53+
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
5454
<PackageReference Include="Newtonsoft.Json" Version="13.*" />
5555
<PackageReference Include="coverlet.collector" Version="3.1.2">
5656
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

0 commit comments

Comments
 (0)