Add cancellation token to search fields in admin (#1110)

This commit is contained in:
Leendert de Borst
2025-08-11 22:21:39 +02:00
committed by Leendert de Borst
parent 6eb8266d05
commit 27fc298b5e
4 changed files with 334 additions and 231 deletions

View File

@@ -15,7 +15,7 @@
</svg>
Email Storage Stats
</a>
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
<RefreshButton OnClick="() => RefreshData(CancellationToken.None)" ButtonText="Refresh" />
</CustomActions>
</PageHeader>
@@ -88,6 +88,7 @@ else
private int PageSize { get; set; } = 50;
private int TotalRecords { get; set; }
private string _searchTerm = string.Empty;
private CancellationTokenSource? _searchCancellationTokenSource;
/// <summary>
/// The last search term.
@@ -102,7 +103,9 @@ else
if (_searchTerm != value)
{
_searchTerm = value;
_ = RefreshData();
_searchCancellationTokenSource?.Cancel();
_searchCancellationTokenSource = new CancellationTokenSource();
_ = RefreshData(_searchCancellationTokenSource.Token);
}
}
}
@@ -113,7 +116,7 @@ else
{
SortColumn = sort.column;
SortDirection = sort.direction;
await RefreshData();
await RefreshData(CancellationToken.None);
}
/// <inheritdoc />
@@ -134,53 +137,65 @@ else
_searchTerm = SearchTermFromQuery;
}
await RefreshData();
await RefreshData(CancellationToken.None);
}
}
private void HandlePageChanged(int newPage)
{
CurrentPage = newPage;
_ = RefreshData();
_ = RefreshData(CancellationToken.None);
}
private async Task RefreshData()
private async Task RefreshData(CancellationToken cancellationToken = default)
{
IsLoading = true;
StateHasChanged();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
IQueryable<Email> query = dbContext.Emails;
query = ApplySearchFilter(query);
query = ApplySort(query);
TotalRecords = await query.CountAsync();
var emailList = await query
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.ToListAsync();
// Get all usernames for the emails in the current list
var encryptionKeyIds = emailList.Select(x => x.UserEncryptionKeyId).Distinct().ToList();
var encryptionKeyUsernames = await dbContext.UserEncryptionKeys
.Where(x => encryptionKeyIds.Contains(x.Id))
.Join(dbContext.AliasVaultUsers, x => x.UserId, y => y.Id, (x, y) => new { EncryptionKeyId = x.Id, UserId = y.Id, y.UserName })
.ToListAsync();
// Create new list of viewmodels
EmailViewModelList = new List<EmailViewModel>();
foreach (var email in emailList)
try
{
var encryptionKey = encryptionKeyUsernames.FirstOrDefault(x => x.EncryptionKeyId == email.UserEncryptionKeyId);
EmailViewModelList.Add(new EmailViewModel { Email = email, UserId = encryptionKey?.UserId ?? string.Empty, UserName = encryptionKey?.UserName ?? string.Empty });
}
IsLoading = true;
StateHasChanged();
IsLoading = false;
IsInitialized = true;
StateHasChanged();
await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken);
IQueryable<Email> query = dbContext.Emails;
query = ApplySearchFilter(query);
query = ApplySort(query);
TotalRecords = await query.CountAsync(cancellationToken);
var emailList = await query
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.ToListAsync(cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
// Get all usernames for the emails in the current list
var encryptionKeyIds = emailList.Select(x => x.UserEncryptionKeyId).Distinct().ToList();
var encryptionKeyUsernames = await dbContext.UserEncryptionKeys
.Where(x => encryptionKeyIds.Contains(x.Id))
.Join(dbContext.AliasVaultUsers, x => x.UserId, y => y.Id, (x, y) => new { EncryptionKeyId = x.Id, UserId = y.Id, y.UserName })
.ToListAsync(cancellationToken);
// Create new list of viewmodels
EmailViewModelList = new List<EmailViewModel>();
foreach (var email in emailList)
{
var encryptionKey = encryptionKeyUsernames.FirstOrDefault(x => x.EncryptionKeyId == email.UserEncryptionKeyId);
EmailViewModelList.Add(new EmailViewModel { Email = email, UserId = encryptionKey?.UserId ?? string.Empty, UserName = encryptionKey?.UserName ?? string.Empty });
}
IsLoading = false;
IsInitialized = true;
StateHasChanged();
}
catch (OperationCanceledException)
{
// Expected when cancellation is requested, do nothing
}
}
/// <summary>
@@ -254,4 +269,14 @@ else
public string UserId { get; set; } = string.Empty;
public string UserName { get; set; } = string.Empty;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_searchCancellationTokenSource?.Cancel();
_searchCancellationTokenSource?.Dispose();
}
base.Dispose(disposing);
}
}

View File

@@ -14,7 +14,7 @@
Description="This page shows an overview of recent auth attempts.">
<CustomActions>
<DeleteButton OnClick="DeleteLogsWithConfirmation" ButtonText="Delete all logs" />
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
<RefreshButton OnClick="() => RefreshData(CancellationToken.None)" ButtonText="Refresh" />
</CustomActions>
</PageHeader>
@@ -107,6 +107,8 @@ else
private string _searchTerm = string.Empty;
private string _lastSearchTerm = string.Empty;
private CancellationTokenSource? _searchCancellationTokenSource;
private string SearchTerm
{
get => _searchTerm;
@@ -115,7 +117,9 @@ else
if (_searchTerm != value)
{
_searchTerm = value;
_ = RefreshData();
_searchCancellationTokenSource?.Cancel();
_searchCancellationTokenSource = new CancellationTokenSource();
_ = RefreshData(_searchCancellationTokenSource.Token);
}
}
}
@@ -129,7 +133,9 @@ else
if (_selectedEventType != value)
{
_selectedEventType = value;
_ = RefreshData();
_searchCancellationTokenSource?.Cancel();
_searchCancellationTokenSource = new CancellationTokenSource();
_ = RefreshData(_searchCancellationTokenSource.Token);
}
}
}
@@ -139,7 +145,7 @@ else
private void ToggleUniqueUsernames()
{
ShowUniqueUsernames = !ShowUniqueUsernames;
_ = RefreshData();
_ = RefreshData(CancellationToken.None);
}
private string SortColumn { get; set; } = "Id";
@@ -149,7 +155,7 @@ else
{
SortColumn = sort.column;
SortDirection = sort.direction;
await RefreshData();
await RefreshData(CancellationToken.None);
}
/// <inheritdoc />
@@ -161,7 +167,7 @@ else
Navigation.LocationChanged += OnLocationChanged;
ParseQueryAndRefresh();
await RefreshData();
await RefreshData(CancellationToken.None);
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
@@ -186,7 +192,7 @@ else
if (_searchTerm != _lastSearchTerm)
{
_lastSearchTerm = _searchTerm;
_ = RefreshData(); // Fire and forget
_ = RefreshData(CancellationToken.None); // Fire and forget
}
}
@@ -194,96 +200,115 @@ else
public void Dispose()
{
Navigation.LocationChanged -= OnLocationChanged;
_searchCancellationTokenSource?.Cancel();
_searchCancellationTokenSource?.Dispose();
}
private void HandlePageChanged(int newPage)
{
CurrentPage = newPage;
_ = RefreshData();
_ = RefreshData(CancellationToken.None);
}
private async Task RefreshData()
private async Task RefreshData(CancellationToken cancellationToken = default)
{
IsLoading = true;
StateHasChanged();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
var query = dbContext.AuthLogs.AsQueryable();
if (!string.IsNullOrEmpty(SearchTerm))
try
{
// Reset page number back to 1 if the search term has changed.
if (SearchTerm != _lastSearchTerm)
{
CurrentPage = 1;
}
_lastSearchTerm = SearchTerm;
IsLoading = true;
StateHasChanged();
// If the search term starts with "client:", we search for the client header.
if (SearchTerm.StartsWith("client:", StringComparison.OrdinalIgnoreCase))
await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken);
var query = dbContext.AuthLogs.AsQueryable();
if (!string.IsNullOrEmpty(SearchTerm))
{
var clientSearchTerm = SearchTerm.Substring(7).ToLower();
query = query.Where(x => EF.Functions.Like((x.Client ?? string.Empty).ToLower(), "%" + clientSearchTerm + "%"));
// Reset page number back to 1 if the search term has changed.
if (SearchTerm != _lastSearchTerm)
{
CurrentPage = 1;
}
_lastSearchTerm = SearchTerm;
// If the search term starts with "client:", we search for the client header.
if (SearchTerm.StartsWith("client:", StringComparison.OrdinalIgnoreCase))
{
var clientSearchTerm = SearchTerm.Substring(7).ToLower();
query = query.Where(x => EF.Functions.Like((x.Client ?? string.Empty).ToLower(), "%" + clientSearchTerm + "%"));
}
else
{
var searchTerm = SearchTerm.Trim().ToLower();
query = query.Where(x => EF.Functions.Like((x.Username ?? string.Empty).ToLower(), "%" + searchTerm + "%") ||
EF.Functions.Like((x.IpAddress ?? string.Empty).ToLower(), "%" + searchTerm + "%"));
}
}
if (!string.IsNullOrEmpty(SelectedEventType))
{
var success = Enum.TryParse<AuthEventType>(SelectedEventType, out var eventType);
if (success)
{
query = query.Where(x => x.EventType == eventType);
}
}
query = ApplySort(query);
// Handle unique usernames filtering after getting all results (since GroupBy with OrderBy in Select is complex for EF)
if (ShowUniqueUsernames)
{
// Get all matching records first
var allLogs = await query.ToListAsync(cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
// Group by username and take the latest entry for each username
var uniqueLogs = allLogs
.GroupBy(x => x.Username)
.Select(g => g.OrderByDescending(x => x.Timestamp).First())
.ToList();
// Apply pagination to the unique results
TotalRecords = uniqueLogs.Count;
LogList = uniqueLogs
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.ToList();
}
else
{
var searchTerm = SearchTerm.Trim().ToLower();
query = query.Where(x => EF.Functions.Like((x.Username ?? string.Empty).ToLower(), "%" + searchTerm + "%") ||
EF.Functions.Like((x.IpAddress ?? string.Empty).ToLower(), "%" + searchTerm + "%"));
TotalRecords = await query.CountAsync(cancellationToken);
LogList = await query
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.ToListAsync(cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
}
}
if (!string.IsNullOrEmpty(SelectedEventType))
// Create user lookup dictionary for the current page
var usernames = LogList.Select(x => x.Username).Distinct().Where(x => !string.IsNullOrEmpty(x)).ToList();
var users = await dbContext.AliasVaultUsers
.Where(u => u.UserName != null && usernames.Contains(u.UserName))
.Select(u => new { u.UserName, u.Id })
.ToListAsync(cancellationToken);
UserLookup = users.Where(u => u.UserName != null).ToDictionary(u => u.UserName!, u => u.Id);
IsLoading = false;
IsInitialized = true;
StateHasChanged();
}
catch (OperationCanceledException)
{
var success = Enum.TryParse<AuthEventType>(SelectedEventType, out var eventType);
if (success)
{
query = query.Where(x => x.EventType == eventType);
}
// Expected when cancellation is requested, do nothing
}
query = ApplySort(query);
// Handle unique usernames filtering after getting all results (since GroupBy with OrderBy in Select is complex for EF)
if (ShowUniqueUsernames)
{
// Get all matching records first
var allLogs = await query.ToListAsync();
// Group by username and take the latest entry for each username
var uniqueLogs = allLogs
.GroupBy(x => x.Username)
.Select(g => g.OrderByDescending(x => x.Timestamp).First())
.ToList();
// Apply pagination to the unique results
TotalRecords = uniqueLogs.Count;
LogList = uniqueLogs
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.ToList();
}
else
{
TotalRecords = await query.CountAsync();
LogList = await query
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.ToListAsync();
}
// Create user lookup dictionary for the current page
var usernames = LogList.Select(x => x.Username).Distinct().Where(x => !string.IsNullOrEmpty(x)).ToList();
var users = await dbContext.AliasVaultUsers
.Where(u => u.UserName != null && usernames.Contains(u.UserName))
.Select(u => new { u.UserName, u.Id })
.ToListAsync();
UserLookup = users.Where(u => u.UserName != null).ToDictionary(u => u.UserName!, u => u.Id);
IsLoading = false;
IsInitialized = true;
StateHasChanged();
}
/// <summary>
@@ -356,7 +381,7 @@ else
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
dbContext.AuthLogs.RemoveRange(dbContext.AuthLogs);
await dbContext.SaveChangesAsync();
await RefreshData();
await RefreshData(CancellationToken.None);
IsLoading = false;
StateHasChanged();

View File

@@ -10,7 +10,7 @@
Description="This page shows an overview of recent system logs.">
<CustomActions>
<DeleteButton OnClick="DeleteLogsWithConfirmation" ButtonText="Delete all logs" />
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
<RefreshButton OnClick="() => RefreshData(CancellationToken.None)" ButtonText="Refresh" />
</CustomActions>
</PageHeader>
@@ -102,6 +102,8 @@ else
private string _searchTerm = string.Empty;
private string _lastSearchTerm = string.Empty;
private CancellationTokenSource? _searchCancellationTokenSource;
private string SearchTerm
{
get => _searchTerm;
@@ -110,7 +112,9 @@ else
if (_searchTerm != value)
{
_searchTerm = value;
_ = RefreshData();
_searchCancellationTokenSource?.Cancel();
_searchCancellationTokenSource = new CancellationTokenSource();
_ = RefreshData(_searchCancellationTokenSource.Token);
}
}
}
@@ -124,7 +128,9 @@ else
if (_selectedServiceName != value)
{
_selectedServiceName = value;
_ = RefreshData();
_searchCancellationTokenSource?.Cancel();
_searchCancellationTokenSource = new CancellationTokenSource();
_ = RefreshData(_searchCancellationTokenSource.Token);
}
}
}
@@ -138,7 +144,7 @@ else
{
SortColumn = sort.column;
SortDirection = sort.direction;
await RefreshData();
await RefreshData(CancellationToken.None);
}
/// <inheritdoc />
@@ -155,66 +161,78 @@ else
{
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
ServiceNames = await dbContext.Logs.Select(l => l.Application).Distinct().ToListAsync();
await RefreshData();
await RefreshData(CancellationToken.None);
}
}
private void HandlePageChanged(int newPage)
{
CurrentPage = newPage;
_ = RefreshData();
_ = RefreshData(CancellationToken.None);
}
private async Task RefreshData()
private async Task RefreshData(CancellationToken cancellationToken = default)
{
IsLoading = true;
StateHasChanged();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
var query = dbContext.Logs.AsQueryable();
query = ApplySearchTermFilter(query);
query = ApplyServiceNameFilter(query);
// Apply sort.
switch (SortColumn)
try
{
case "Application":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Application)
: query.OrderByDescending(x => x.Application);
break;
case "Message":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Message)
: query.OrderByDescending(x => x.Message);
break;
case "Level":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Level)
: query.OrderByDescending(x => x.Level);
break;
case "Timestamp":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.TimeStamp)
: query.OrderByDescending(x => x.TimeStamp);
break;
default:
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Id)
: query.OrderByDescending(x => x.Id);
break;
IsLoading = true;
StateHasChanged();
await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken);
var query = dbContext.Logs.AsQueryable();
query = ApplySearchTermFilter(query);
query = ApplyServiceNameFilter(query);
// Apply sort.
switch (SortColumn)
{
case "Application":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Application)
: query.OrderByDescending(x => x.Application);
break;
case "Message":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Message)
: query.OrderByDescending(x => x.Message);
break;
case "Level":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Level)
: query.OrderByDescending(x => x.Level);
break;
case "Timestamp":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.TimeStamp)
: query.OrderByDescending(x => x.TimeStamp);
break;
default:
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Id)
: query.OrderByDescending(x => x.Id);
break;
}
TotalRecords = await query.CountAsync(cancellationToken);
LogList = await query
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.ToListAsync(cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
IsLoading = false;
IsInitialized = true;
StateHasChanged();
}
catch (OperationCanceledException)
{
// Expected when cancellation is requested, do nothing
}
TotalRecords = await query.CountAsync();
LogList = await query
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.ToListAsync();
IsLoading = false;
IsInitialized = true;
StateHasChanged();
}
/// <summary>
@@ -272,9 +290,19 @@ else
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
dbContext.Logs.RemoveRange(dbContext.Logs);
await dbContext.SaveChangesAsync();
await RefreshData();
await RefreshData(CancellationToken.None);
IsLoading = false;
StateHasChanged();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_searchCancellationTokenSource?.Cancel();
_searchCancellationTokenSource?.Dispose();
}
base.Dispose(disposing);
}
}

View File

@@ -9,7 +9,7 @@
Title="@(TotalRecords > 0 ? $"Users ({TotalRecords:N0})" : "Users")"
Description="This page shows an overview of all registered users and the associated vaults.">
<CustomActions>
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
<RefreshButton OnClick="() => RefreshData(CancellationToken.None)" ButtonText="Refresh" />
</CustomActions>
</PageHeader>
@@ -86,6 +86,7 @@ else
private int TotalRecords { get; set; }
private string _searchTerm = string.Empty;
private CancellationTokenSource? _searchCancellationTokenSource;
/// <summary>
/// The last search term.
@@ -100,7 +101,9 @@ else
if (_searchTerm != value)
{
_searchTerm = value;
_ = RefreshData();
_searchCancellationTokenSource?.Cancel();
_searchCancellationTokenSource = new CancellationTokenSource();
_ = RefreshData(_searchCancellationTokenSource.Token);
}
}
}
@@ -112,7 +115,7 @@ else
{
SortColumn = sort.column;
SortDirection = sort.direction;
await RefreshData();
await RefreshData(CancellationToken.None);
}
/// <inheritdoc />
@@ -127,72 +130,84 @@ else
{
if (firstRender)
{
await RefreshData();
await RefreshData(CancellationToken.None);
}
}
private void HandlePageChanged(int newPage)
{
CurrentPage = newPage;
_ = RefreshData();
_ = RefreshData(CancellationToken.None);
}
private async Task RefreshData()
private async Task RefreshData(CancellationToken cancellationToken = default)
{
IsLoading = true;
StateHasChanged();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
IQueryable<AliasVaultUser> query = dbContext.AliasVaultUsers;
query = ApplySearchFilter(query);
query = ApplySort(query, dbContext);
TotalRecords = await query.CountAsync();
var users = await query
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.Select(u => new
{
u.Id,
u.UserName,
u.CreatedAt,
u.TwoFactorEnabled,
u.Blocked,
Vaults = u.Vaults.Select(v => new
{
v.FileSize,
v.CreatedAt,
v.RevisionNumber,
CredentialCount = v.CredentialsCount,
}),
EmailClaims = u.EmailClaims.Select(ec => new
{
ec.CreatedAt,
ec.Address
}),
ReceivedEmails = u.EmailClaims.SelectMany(ec => dbContext.Emails.Where(e => e.To == ec.Address)).Count(),
})
.ToListAsync();
UserList = users.Select(user => new UserViewModel
try
{
Id = user.Id,
UserName = user.UserName?.ToLower() ?? "N/A",
TwoFactorEnabled = user.TwoFactorEnabled,
Blocked = user.Blocked,
CreatedAt = user.CreatedAt,
VaultCount = user.Vaults.Count(),
CredentialCount = user.Vaults.OrderByDescending(x => x.RevisionNumber).First().CredentialCount,
EmailClaimCount = user.EmailClaims.Count(),
ReceivedEmailCount = user.ReceivedEmails,
VaultStorageInKb = user.Vaults.Sum(x => x.FileSize),
LastVaultUpdate = user.Vaults.Any() ? user.Vaults.Max(x => x.CreatedAt) : user.CreatedAt,
}).ToList();
IsLoading = true;
StateHasChanged();
IsLoading = false;
IsInitialized = true;
StateHasChanged();
await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken);
IQueryable<AliasVaultUser> query = dbContext.AliasVaultUsers;
query = ApplySearchFilter(query);
query = ApplySort(query, dbContext);
TotalRecords = await query.CountAsync(cancellationToken);
var users = await query
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.Select(u => new
{
u.Id,
u.UserName,
u.CreatedAt,
u.TwoFactorEnabled,
u.Blocked,
Vaults = u.Vaults.Select(v => new
{
v.FileSize,
v.CreatedAt,
v.RevisionNumber,
CredentialCount = v.CredentialsCount,
}),
EmailClaims = u.EmailClaims.Select(ec => new
{
ec.CreatedAt,
ec.Address
}),
ReceivedEmails = u.EmailClaims.SelectMany(ec => dbContext.Emails.Where(e => e.To == ec.Address)).Count(),
})
.ToListAsync(cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
UserList = users.Select(user => new UserViewModel
{
Id = user.Id,
UserName = user.UserName?.ToLower() ?? "N/A",
TwoFactorEnabled = user.TwoFactorEnabled,
Blocked = user.Blocked,
CreatedAt = user.CreatedAt,
VaultCount = user.Vaults.Count(),
CredentialCount = user.Vaults.OrderByDescending(x => x.RevisionNumber).First().CredentialCount,
EmailClaimCount = user.EmailClaims.Count(),
ReceivedEmailCount = user.ReceivedEmails,
VaultStorageInKb = user.Vaults.Sum(x => x.FileSize),
LastVaultUpdate = user.Vaults.Any() ? user.Vaults.Max(x => x.CreatedAt) : user.CreatedAt,
}).ToList();
IsLoading = false;
IsInitialized = true;
StateHasChanged();
}
catch (OperationCanceledException)
{
// Expected when cancellation is requested, do nothing
}
}
/// <summary>
@@ -279,4 +294,14 @@ else
return query;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_searchCancellationTokenSource?.Cancel();
_searchCancellationTokenSource?.Dispose();
}
base.Dispose(disposing);
}
}