From fa343520a850a2dbfd884bd66aa0e5a13cc609e5 Mon Sep 17 00:00:00 2001 From: Miguel Hasse de Oliveira Date: Thu, 12 Sep 2024 16:57:39 +0100 Subject: [PATCH 1/2] Fix: A second operation was started in DataGrid with EF Core (#801) --- ...crosoft.FluentUI.AspNetCore.Components.xml | 30 ++++++- .../DataGrid/FluentDataGrid.razor.cs | 89 +++++++++++++------ .../Infrastructure/IAsyncQueryExecutor.cs | 6 +- ...eworkAdapterServiceCollectionExtensions.cs | 2 +- .../EntityFrameworkAsyncQueryExecutor.cs | 28 ++++-- 5 files changed, 120 insertions(+), 35 deletions(-) diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index d2911222f3..3080784032 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -2011,6 +2011,13 @@ The title of the column to sort by. The direction of sorting. The default is . If the value is , then it will toggle the direction on each call. + A representing the completion of the operation. + + + + Removes the grid's sort on double click for the currently sorted column if it's not a default sort column. + + A representing the completion of the operation. @@ -2018,6 +2025,7 @@ The index of the column to sort by. The direction of sorting. The default is . If the value is , then it will toggle the direction on each call. + A representing the completion of the operation. @@ -2032,6 +2040,21 @@ options UI that was previously displayed. The column whose options are to be displayed, if any are available. + A representing the completion of the operation. + + + + Displays the UI for the specified column found first, + closing any other column options UI that was previously displayed. If the title is not found, nothing happens. + + A representing the completion of the operation. + + + + Displays the UI for the specified column , + closing any other column options UI that was previously displayed. If the index is out of range, nothing happens. + + A representing the completion of the operation. @@ -2039,6 +2062,7 @@ resize UI that was previously displayed. The column whose resize UI is to be displayed. + A representing the completion of the operation. @@ -2263,19 +2287,21 @@ An instance. True if this instance can perform asynchronous queries for the supplied , otherwise false. - + Asynchronously counts the items in the , if supported. The data type. An instance. + An instance. The number of items in .. - + Asynchronously materializes the as an array, if supported. The data type. + An instance. An instance. The items in the .. diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs index a72e9acc50..f7160f50ca 100644 --- a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web.Virtualization; +using Microsoft.Extensions.DependencyInjection; using Microsoft.FluentUI.AspNetCore.Components.DataGrid.Infrastructure; using Microsoft.FluentUI.AspNetCore.Components.Extensions; using Microsoft.FluentUI.AspNetCore.Components.Infrastructure; @@ -27,7 +28,7 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve private LibraryConfiguration LibraryConfiguration { get; set; } = default!; [Inject] - private IServiceProvider Services { get; set; } = default!; + private IServiceScopeFactory ScopeFactory { get; set; } = default!; [Inject] private IJSRuntime JSRuntime { get; set; } = default!; @@ -254,6 +255,7 @@ public partial class FluentDataGrid : FluentComponentBase, IHandleEve // IQueryable only exposes synchronous query APIs. IAsyncQueryExecutor is an adapter that lets us invoke any // async query APIs that might be available. We have built-in support for using EF Core's async query APIs. private IAsyncQueryExecutor? _asyncQueryExecutor; + private AsyncServiceScope? _scope; // We cascade the InternalGridContext to descendants, which in turn call it to add themselves to _columns // This happens on every render so that the column list can be updated dynamically @@ -350,9 +352,11 @@ protected override Task OnParametersSetAsync() var dataSourceHasChanged = !Equals(Items, _lastAssignedItems) || !Equals(ItemsProvider, _lastAssignedItemsProvider); if (dataSourceHasChanged) { + _scope?.Dispose(); + _scope = ScopeFactory.CreateAsyncScope(); _lastAssignedItemsProvider = ItemsProvider; _lastAssignedItems = Items; - _asyncQueryExecutor = AsyncQueryExecutorSupplier.GetAsyncQueryExecutor(Services, Items); + _asyncQueryExecutor = AsyncQueryExecutorSupplier.GetAsyncQueryExecutor(_scope.Value.ServiceProvider, Items); } var paginationStateHasChanged = @@ -470,6 +474,7 @@ public Task SortByColumnAsync(ColumnBase column, SortDirection direct /// /// The title of the column to sort by. /// The direction of sorting. The default is . If the value is , then it will toggle the direction on each call. + /// A representing the completion of the operation. public Task SortByColumnAsync(string title, SortDirection direction = SortDirection.Auto) { var column = _columns.FirstOrDefault(c => c.Title?.Equals(title, StringComparison.InvariantCultureIgnoreCase) ?? false); @@ -477,11 +482,18 @@ public Task SortByColumnAsync(string title, SortDirection direction = SortDirect return column is not null ? SortByColumnAsync(column, direction) : Task.CompletedTask; } + /// + /// Removes the grid's sort on double click for the currently sorted column if it's not a default sort column. + /// + /// A representing the completion of the operation. + public Task RemoveSortByColumnAsync() => (_sortByColumn != null) ? RemoveSortByColumnAsync(_sortByColumn) : Task.CompletedTask; + /// /// Sorts the grid by the specified column . If the index is out of range, nothing happens. /// /// The index of the column to sort by. /// The direction of sorting. The default is . If the value is , then it will toggle the direction on each call. + /// A representing the completion of the operation. public Task SortByColumnAsync(int index, SortDirection direction = SortDirection.Auto) { return index >= 0 && index < _columns.Count ? SortByColumnAsync(_columns[index], direction) : Task.CompletedTask; @@ -509,6 +521,7 @@ public Task RemoveSortByColumnAsync(ColumnBase column) /// options UI that was previously displayed. /// /// The column whose options are to be displayed, if any are available. + /// A representing the completion of the operation. public Task ShowColumnOptionsAsync(ColumnBase column) { _displayOptionsForColumn = column; @@ -517,11 +530,33 @@ public Task ShowColumnOptionsAsync(ColumnBase column) return Task.CompletedTask; } + /// + /// Displays the UI for the specified column found first, + /// closing any other column options UI that was previously displayed. If the title is not found, nothing happens. + /// + /// A representing the completion of the operation. + public Task ShowColumnOptionsAsync(string title) + { + var column = _columns.FirstOrDefault(c => c.Title?.Equals(title, StringComparison.InvariantCultureIgnoreCase) ?? false); + return (column is not null) ? ShowColumnOptionsAsync(column) : Task.CompletedTask; + } + + /// + /// Displays the UI for the specified column , + /// closing any other column options UI that was previously displayed. If the index is out of range, nothing happens. + /// + /// A representing the completion of the operation. + public Task ShowColumnOptionsAsync(int index) + { + return (index >= 0 && index < _columns.Count) ? ShowColumnOptionsAsync(_columns[index]) : Task.CompletedTask; + } + /// /// Displays the column resize UI for the specified column, closing any other column /// resize UI that was previously displayed. /// /// The column whose resize UI is to be displayed. + /// A representing the completion of the operation. public Task ShowColumnResizeAsync(ColumnBase column) { _displayResizeForColumn = column; @@ -540,10 +575,7 @@ public void SetLoadingState(bool loading) /// (either or ). /// /// A that represents the completion of the operation. - public async Task RefreshDataAsync() - { - await RefreshDataCoreAsync(); - } + public Task RefreshDataAsync() => RefreshDataCoreAsync(); // Same as RefreshDataAsync, except without forcing a re-render. We use this from OnParametersSetAsync // because in that case there's going to be a re-render anyway. @@ -639,31 +671,37 @@ private async Task RefreshDataCoreAsync() // Normalizes all the different ways of configuring a data source so they have common GridItemsProvider-shaped API private async ValueTask> ResolveItemsRequestAsync(GridItemsProviderRequest request) { - if (ItemsProvider is not null) + try { - var gipr = await ItemsProvider(request); - if (gipr.Items is not null) + if (ItemsProvider is not null) { - Loading = false; + var gipr = await ItemsProvider(request); + if (gipr.Items is not null) + { + Loading = false; + } + return gipr; } - return gipr; - } - else if (Items is not null) - { - var totalItemCount = _asyncQueryExecutor is null ? Items.Count() : await _asyncQueryExecutor.CountAsync(Items); - _internalGridContext.TotalItemCount = totalItemCount; - var result = request.ApplySorting(Items).Skip(request.StartIndex); - if (request.Count.HasValue) + else if (Items is not null) { - result = result.Take(request.Count.Value); + var totalItemCount = _asyncQueryExecutor is null ? Items.Count() : await _asyncQueryExecutor.CountAsync(Items, request.CancellationToken); + _internalGridContext.TotalItemCount = totalItemCount; + var result = request.ApplySorting(Items).Skip(request.StartIndex); + if (request.Count.HasValue) + { + result = result.Take(request.Count.Value); + } + var resultArray = _asyncQueryExecutor is null ? [.. result] : await _asyncQueryExecutor.ToArrayAsync(result, request.CancellationToken); + return GridItemsProviderResult.From(resultArray, totalItemCount); } - var resultArray = _asyncQueryExecutor is null ? [.. result] : await _asyncQueryExecutor.ToArrayAsync(result); - return GridItemsProviderResult.From(resultArray, totalItemCount); } - else + catch (OperationCanceledException oce) when (oce.CancellationToken == request.CancellationToken) { - return GridItemsProviderResult.From(Array.Empty(), 0); + // No-op; we canceled the operation, so it's fine to suppress this exception. } + + Loading = false; + return GridItemsProviderResult.From(Array.Empty(), 0); } private string AriaSortValue(ColumnBase column) @@ -673,8 +711,8 @@ private string AriaSortValue(ColumnBase column) private string? ColumnHeaderClass(ColumnBase column) => _sortByColumn == column - ? $"{ColumnClass(column)} {(_sortByAscending ? "col-sort-asc" : "col-sort-desc")}" - : ColumnClass(column); + ? $"{ColumnClass(column)} {(_sortByAscending ? "col-sort-asc" : "col-sort-desc")}" + : ColumnClass(column); private string? GridClass() { @@ -700,6 +738,7 @@ private string AriaSortValue(ColumnBase column) public async ValueTask DisposeAsync() { _currentPageItemsChanged.Dispose(); + _scope?.Dispose(); try { diff --git a/src/Core/Components/DataGrid/Infrastructure/IAsyncQueryExecutor.cs b/src/Core/Components/DataGrid/Infrastructure/IAsyncQueryExecutor.cs index 69322a97af..5e6f17d7fb 100644 --- a/src/Core/Components/DataGrid/Infrastructure/IAsyncQueryExecutor.cs +++ b/src/Core/Components/DataGrid/Infrastructure/IAsyncQueryExecutor.cs @@ -18,14 +18,16 @@ public interface IAsyncQueryExecutor /// /// The data type. /// An instance. + /// An instance. /// The number of items in .. - Task CountAsync(IQueryable queryable); + Task CountAsync(IQueryable queryable, CancellationToken cancellationToken = default); /// /// Asynchronously materializes the as an array, if supported. /// /// The data type. + /// An instance. /// An instance. /// The items in the .. - Task ToArrayAsync(IQueryable queryable); + Task ToArrayAsync(IQueryable queryable, CancellationToken cancellationToken = default); } diff --git a/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAdapterServiceCollectionExtensions.cs b/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAdapterServiceCollectionExtensions.cs index 9df6870018..cadf2921ef 100644 --- a/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAdapterServiceCollectionExtensions.cs +++ b/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAdapterServiceCollectionExtensions.cs @@ -14,6 +14,6 @@ public static class EntityFrameworkAdapterServiceCollectionExtensions /// The . public static void AddDataGridEntityFrameworkAdapter(this IServiceCollection services) { - services.AddSingleton(); + services.AddScoped(); } } diff --git a/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAsyncQueryExecutor.cs b/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAsyncQueryExecutor.cs index 43172efa7d..d76546e1a1 100644 --- a/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAsyncQueryExecutor.cs +++ b/src/Extensions/DataGrid.EntityFrameworkAdapter/EntityFrameworkAsyncQueryExecutor.cs @@ -4,14 +4,32 @@ namespace Microsoft.FluentUI.AspNetCore.Components.DataGrid.EntityFrameworkAdapter; -internal class EntityFrameworkAsyncQueryExecutor : IAsyncQueryExecutor +internal class EntityFrameworkAsyncQueryExecutor : IAsyncQueryExecutor, IDisposable { + private readonly SemaphoreSlim _lock = new(1); + public bool IsSupported(IQueryable queryable) => queryable.Provider is IAsyncQueryProvider; - public Task CountAsync(IQueryable queryable) - => queryable.CountAsync(); + public Task CountAsync(IQueryable queryable, CancellationToken cancellationToken) + => ExecuteAsync(() => queryable.CountAsync(cancellationToken)); + + public Task ToArrayAsync(IQueryable queryable, CancellationToken cancellationToken) + => ExecuteAsync(() => queryable.ToArrayAsync(cancellationToken)); + + private async Task ExecuteAsync(Func> operation) + { + await _lock.WaitAsync(); + + try + { + return await operation(); + } + finally + { + _lock.Release(); + } + } - public Task ToArrayAsync(IQueryable queryable) - => queryable.ToArrayAsync(); + void IDisposable.Dispose() => _lock.Dispose(); } From 315f06ad131f58052117bcc5555046b21404214b Mon Sep 17 00:00:00 2001 From: Miguel Hasse de Oliveira Date: Mon, 23 Sep 2024 15:48:54 +0100 Subject: [PATCH 2/2] Removed additions not relevant for the PR subject as requested by Vincent Baaij --- ...crosoft.FluentUI.AspNetCore.Components.xml | 20 ------------ .../DataGrid/FluentDataGrid.razor.cs | 32 +++---------------- 2 files changed, 4 insertions(+), 48 deletions(-) diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index 51451aae2e..9ffb41a6ef 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -2014,12 +2014,6 @@ The direction of sorting. The default is . If the value is , then it will toggle the direction on each call. A representing the completion of the operation. - - - Removes the grid's sort on double click for the currently sorted column if it's not a default sort column. - - A representing the completion of the operation. - Sorts the grid by the specified column . If the index is out of range, nothing happens. @@ -2043,20 +2037,6 @@ The column whose options are to be displayed, if any are available. A representing the completion of the operation. - - - Displays the UI for the specified column found first, - closing any other column options UI that was previously displayed. If the title is not found, nothing happens. - - A representing the completion of the operation. - - - - Displays the UI for the specified column , - closing any other column options UI that was previously displayed. If the index is out of range, nothing happens. - - A representing the completion of the operation. - Displays the column resize UI for the specified column, closing any other column diff --git a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs index a5893a54b2..04ab45fc9f 100644 --- a/src/Core/Components/DataGrid/FluentDataGrid.razor.cs +++ b/src/Core/Components/DataGrid/FluentDataGrid.razor.cs @@ -483,12 +483,6 @@ public Task SortByColumnAsync(string title, SortDirection direction = SortDirect return column is not null ? SortByColumnAsync(column, direction) : Task.CompletedTask; } - /// - /// Removes the grid's sort on double click for the currently sorted column if it's not a default sort column. - /// - /// A representing the completion of the operation. - public Task RemoveSortByColumnAsync() => (_sortByColumn != null) ? RemoveSortByColumnAsync(_sortByColumn) : Task.CompletedTask; - /// /// Sorts the grid by the specified column . If the index is out of range, nothing happens. /// @@ -531,27 +525,6 @@ public Task ShowColumnOptionsAsync(ColumnBase column) return Task.CompletedTask; } - /// - /// Displays the UI for the specified column found first, - /// closing any other column options UI that was previously displayed. If the title is not found, nothing happens. - /// - /// A representing the completion of the operation. - public Task ShowColumnOptionsAsync(string title) - { - var column = _columns.FirstOrDefault(c => c.Title?.Equals(title, StringComparison.InvariantCultureIgnoreCase) ?? false); - return (column is not null) ? ShowColumnOptionsAsync(column) : Task.CompletedTask; - } - - /// - /// Displays the UI for the specified column , - /// closing any other column options UI that was previously displayed. If the index is out of range, nothing happens. - /// - /// A representing the completion of the operation. - public Task ShowColumnOptionsAsync(int index) - { - return (index >= 0 && index < _columns.Count) ? ShowColumnOptionsAsync(_columns[index]) : Task.CompletedTask; - } - /// /// Displays the column resize UI for the specified column, closing any other column /// resize UI that was previously displayed. @@ -576,7 +549,10 @@ public void SetLoadingState(bool loading) /// (either or ). /// /// A that represents the completion of the operation. - public Task RefreshDataAsync() => RefreshDataCoreAsync(); + public async Task RefreshDataAsync() + { + await RefreshDataCoreAsync(); + } // Same as RefreshDataAsync, except without forcing a re-render. We use this from OnParametersSetAsync // because in that case there's going to be a re-render anyway.