Skip to content

Commit e9f9641

Browse files
committed
feat: Add CancellationToken support to all asynchronous service methods and propagate it to underlying client calls.
1 parent 38879b9 commit e9f9641

11 files changed

Lines changed: 253 additions & 133 deletions

File tree

src/BookStore.Web/Components/Pages/Admin/AddBookDialog.razor

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
@inject IDialogService DialogService
99
@inject ISnackbar Snackbar
1010

11+
@implements IDisposable
1112
<MudDialog>
1213
<TitleContent>
1314
<MudText Typo="Typo.h6">
@@ -151,6 +152,8 @@
151152
private string _selectedLanguage = "en-US";
152153
private string _currentDescription = "";
153154
private Dictionary<string, string> _translations = new();
155+
private readonly CancellationTokenSource _cts = new();
156+
private bool _disposed;
154157

155158
protected override void OnInitialized()
156159
{
@@ -335,13 +338,13 @@
335338
};
336339
updateRequest.AdditionalProperties["prices"] = _model.AdditionalProperties["prices"];
337340

338-
await BooksClient.UpdateBookAsync(BookId, updateRequest, ETag);
341+
await BooksClient.UpdateBookAsync(BookId, updateRequest, ETag, _cts.Token);
339342
Snackbar.Add("Book updated successfully.", Severity.Success);
340343
}
341344
else
342345
{
343346
_model.Id = Guid.CreateVersion7();
344-
await BooksClient.CreateBookAsync(_model);
347+
await BooksClient.CreateBookAsync(_model, _cts.Token);
345348
Snackbar.Add("Book created successfully.", Severity.Success);
346349
}
347350

@@ -354,4 +357,14 @@
354357
}
355358

356359
private void Cancel() => MudDialog.Cancel();
360+
361+
public void Dispose()
362+
{
363+
if (_disposed) return;
364+
_disposed = true;
365+
366+
_cts.Cancel();
367+
_cts.Dispose();
368+
}
369+
357370
}

src/BookStore.Web/Components/Pages/Admin/BookManagement.razor

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@
9595
@code {
9696
private MudTable<AdminBookDto> _table = null!;
9797
private string _searchString = "";
98+
private readonly CancellationTokenSource _cts = new();
99+
private bool _disposed;
98100

99101
protected override void OnInitialized()
100102
{
@@ -104,6 +106,11 @@
104106

105107
public void Dispose()
106108
{
109+
if (_disposed) return;
110+
_disposed = true;
111+
112+
_cts.Cancel();
113+
_cts.Dispose();
107114
EventsService.OnNotificationReceived -= HandleNotification;
108115
}
109116

@@ -230,7 +237,7 @@
230237
{
231238
try
232239
{
233-
await BooksClient.SoftDeleteBookAsync(id, etag);
240+
await BooksClient.SoftDeleteBookAsync(id, etag, _cts.Token);
234241
Snackbar.Add("Book deleted.", Severity.Success);
235242
await _table.ReloadServerData();
236243
}
@@ -244,7 +251,7 @@
244251
{
245252
try
246253
{
247-
await BooksClient.RestoreBookAsync(id, etag: etag);
254+
await BooksClient.RestoreBookAsync(id, etag: etag, cancellationToken: _cts.Token);
248255
Snackbar.Add("Book restored.", Severity.Success);
249256
await _table.ReloadServerData();
250257
}

src/BookStore.Web/Components/Pages/BookDetails.razor

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
@inject NavigationManager Navigation
1111
@inject AuthenticationStateProvider AuthStateProvider
1212
@inject ISnackbar Snackbar
13-
@implements IAsyncDisposable
13+
@implements IDisposable
1414
@rendermode InteractiveServer
1515
@inject CatalogService CatalogService
1616
@inject CurrencyService CurrencyService
@@ -307,6 +307,8 @@
307307

308308
[CascadingParameter] private Task<AuthenticationState> AuthStateTask { get; set; } = default!;
309309
private System.Security.Claims.ClaimsPrincipal? user;
310+
private readonly CancellationTokenSource _cts = new();
311+
private bool _disposed;
310312
private int quantity = 1;
311313

312314
private async Task AddToCartAsync()
@@ -315,7 +317,7 @@
315317

316318
try
317319
{
318-
await CartClient.AddToCartAsync(new BookStore.Client.AddToCartClientRequest(book.Id, quantity));
320+
await CartClient.AddToCartAsync(new BookStore.Client.AddToCartClientRequest(book.Id, quantity), _cts.Token);
319321
Snackbar.Add($"Added {quantity} copy(ies) to cart", Severity.Success);
320322
quantity = 1; // Reset quantity
321323
}
@@ -340,6 +342,7 @@
340342

341343
protected override async Task OnParametersSetAsync()
342344
{
345+
if (_disposed) return;
343346
// Reload when ID parameter changes
344347
// Note: queryFn captures 'Id', so we need to reload if Id changes.
345348
// But ReactiveQuery caches the function. We strictly should allow updating the function or creating new query.
@@ -350,7 +353,7 @@
350353
// But we need to check if it actually changed to avoid loop?
351354
// Actually OnParametersSetAsync runs on standard parameter set.
352355
// We'll manage checking inside InitializeQueryAsync logic or just re-init.
353-
await InitializeQueryAsync();
356+
await InitializeQueryAsync(_cts.Token);
354357
}
355358

356359
var authState = await AuthStateTask;
@@ -361,23 +364,23 @@
361364
user = newUser;
362365
if (user.Identity?.IsAuthenticated == true && bookQuery != null)
363366
{
364-
await bookQuery.LoadAsync(silent: true);
367+
await bookQuery.LoadAsync(silent: true, cancellationToken: _cts.Token);
365368
}
366369
}
367370
}
368371

369-
private async Task InitializeQueryAsync()
372+
private async Task InitializeQueryAsync(CancellationToken cancellationToken = default)
370373
{
371374
// Dispose previous if exists
372375
bookQuery?.Dispose();
373376

374377
bookQuery = new ReactiveQuery<BookDto?>(
375-
queryFn: async () =>
378+
queryFn: async ct =>
376379
{
377380
try
378381
{
379382
return await BooksClient.GetBookAsync(
380-
id: Id);
383+
id: Id, cancellationToken: ct);
381384
}
382385
catch (Refit.ApiException apiEx) when (apiEx.StatusCode == System.Net.HttpStatusCode.NotFound)
383386
{
@@ -387,20 +390,20 @@
387390
eventsService: EventsService,
388391
invalidationService: InvalidationService,
389392
queryKeys: new[] { $"Book:{Id}", "User" },
390-
onStateChanged: StateHasChanged,
393+
onStateChanged: () => InvokeAsync(StateHasChanged),
391394
logger: Logger
392395
);
393396

394-
await bookQuery.LoadAsync();
397+
await bookQuery.LoadAsync(cancellationToken: cancellationToken);
395398
UpdateBreadcrumbs();
396399
}
397400

398-
private async Task<BookDto?> FetchBookAsync()
401+
private async Task<BookDto?> FetchBookAsync(CancellationToken cancellationToken)
399402
{
400403
try
401404
{
402405
return await BooksClient.GetBookAsync(
403-
id: Id);
406+
id: Id, cancellationToken: cancellationToken);
404407
}
405408
catch (Refit.ApiException apiEx) when (apiEx.StatusCode == System.Net.HttpStatusCode.NotFound)
406409
{
@@ -422,9 +425,10 @@
422425

423426
private async Task RetryLoad()
424427
{
428+
if (_disposed) return;
425429
if (bookQuery != null)
426430
{
427-
await bookQuery.LoadAsync();
431+
await bookQuery.LoadAsync(cancellationToken: _cts.Token);
428432
UpdateBreadcrumbs();
429433
}
430434
}
@@ -445,7 +449,8 @@
445449
{
446450
IsFavorite = oldState,
447451
LikeCount = oldState ? current.LikeCount + 1 : current.LikeCount - 1
448-
}));
452+
}),
453+
cancellationToken: _cts.Token);
449454
}
450455

451456
private async Task RateBookAsync(int rating)
@@ -464,7 +469,8 @@
464469
: current with
465470
{
466471
UserRating = oldRating
467-
}));
472+
}),
473+
cancellationToken: _cts.Token);
468474
}
469475

470476
private async Task RemoveRatingAsync()
@@ -483,7 +489,8 @@
483489
: current with
484490
{
485491
UserRating = oldRating
486-
}));
492+
}),
493+
cancellationToken: _cts.Token);
487494
}
488495

489496
private async Task DeleteBookAsync()
@@ -500,9 +507,9 @@
500507

501508
try
502509
{
503-
await BooksClient.SoftDeleteBookAsync(currentBook.Id);
510+
await BooksClient.SoftDeleteBookAsync(currentBook.Id, cancellationToken: _cts.Token);
504511
Snackbar.Add("Book deleted", Severity.Success);
505-
if (bookQuery != null) await bookQuery.LoadAsync();
512+
if (bookQuery != null) await bookQuery.LoadAsync(cancellationToken: _cts.Token);
506513
}
507514
catch (Exception ex)
508515
{
@@ -518,9 +525,9 @@
518525

519526
try
520527
{
521-
await BooksClient.RestoreBookAsync(currentBook.Id);
528+
await BooksClient.RestoreBookAsync(currentBook.Id, cancellationToken: _cts.Token);
522529
Snackbar.Add("Book restored", Severity.Success);
523-
if (bookQuery != null) await bookQuery.LoadAsync();
530+
if (bookQuery != null) await bookQuery.LoadAsync(cancellationToken: _cts.Token);
524531
}
525532
catch (Exception ex)
526533
{
@@ -532,8 +539,13 @@
532539
// Inject EventsService which wasn't injected before
533540
[Inject] private BookStoreEventsService EventsService { get; set; } = default!;
534541

535-
public async ValueTask DisposeAsync()
542+
public void Dispose()
536543
{
544+
if (_disposed) return;
545+
_disposed = true;
546+
547+
_cts.Cancel();
548+
_cts.Dispose();
537549
bookQuery?.Dispose();
538550
CurrencyService.OnCurrencyChanged -= StateHasChanged;
539551
}

0 commit comments

Comments
 (0)