diff --git a/src/Persistence/EntityFramework/PlayerContext.cs b/src/Persistence/EntityFramework/PlayerContext.cs index 034d74e..95df34f 100644 --- a/src/Persistence/EntityFramework/PlayerContext.cs +++ b/src/Persistence/EntityFramework/PlayerContext.cs @@ -100,6 +100,26 @@ internal class PlayerContext : CachingEntityFrameworkContext, IPlayerContext } } + /// + public async ValueTask> SearchAccountsAsync(string searchTerm, int skip, int count, CancellationToken cancellationToken = default) + { + using (this.RepositoryProvider.ContextStack.UseContext(this)) + { + // Invariant: this one runs in .NET, so it must not depend on the server's locale - in a + // Turkish one, "I".ToLower() is a dotless "ı" and the term would match nothing. The + // ToLower() calls inside the query below are a different matter: they are translated to + // the database's own lower(), which is why they cannot take a culture. + var term = searchTerm.ToLowerInvariant(); + return await this.Context.Set().AsNoTracking() + .Where(a => a.LoginName.ToLower().Contains(term) + || a.RawCharacters.Any(c => c.Name.ToLower().Contains(term))) + .OrderBy(a => a.LoginName) + .Skip(skip) + .Take(count) + .ToListAsync(cancellationToken).ConfigureAwait(false); + } + } + /// public async ValueTask GetAccountByCharacterNameAsync(string characterName, CancellationToken cancellationToken = default) { diff --git a/src/Persistence/IPlayerContext.cs b/src/Persistence/IPlayerContext.cs index 80e95fb..93637a0 100644 --- a/src/Persistence/IPlayerContext.cs +++ b/src/Persistence/IPlayerContext.cs @@ -78,6 +78,19 @@ public interface IPlayerContext : IContext /// ValueTask> GetAccountsOrderedByLoginNameAsync(int skip, int count, CancellationToken cancellationToken = default); + /// + /// Gets the accounts which contain the search term in their login name or in one of + /// their character names, ordered by login name. + /// + /// The search term. + /// The skip count. + /// The count. + /// The cancellation token. + /// + /// The account objects, without dependent data. + /// + ValueTask> SearchAccountsAsync(string searchTerm, int skip, int count, CancellationToken cancellationToken = default); + /// /// Gets the account by character name. /// diff --git a/src/Persistence/InMemory/PlayerInMemoryContext.cs b/src/Persistence/InMemory/PlayerInMemoryContext.cs index ad7dfca..bb7f3bb 100644 --- a/src/Persistence/InMemory/PlayerInMemoryContext.cs +++ b/src/Persistence/InMemory/PlayerInMemoryContext.cs @@ -57,6 +57,18 @@ public class PlayerInMemoryContext : InMemoryContext, IPlayerContext return allAccounts.OrderBy(a => a.LoginName).Skip(skip).Take(count); } + /// + public async ValueTask> SearchAccountsAsync(string searchTerm, int skip, int count, CancellationToken cancellationToken = default) + { + var allAccounts = await this.Provider.GetRepository().GetAllAsync(cancellationToken).ConfigureAwait(false); + return allAccounts + .Where(a => a.LoginName.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase) + || a.Characters.Any(c => c.Name.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase))) + .OrderBy(a => a.LoginName) + .Skip(skip) + .Take(count); + } + /// public async ValueTask CanSaveLetterAsync(Interfaces.LetterHeader letterHeader, CancellationToken cancellationToken = default) { diff --git a/src/Web/Shared/Services/AccountService.cs b/src/Web/Shared/Services/AccountService.cs index 9df8db1..03c48f3 100644 --- a/src/Web/Shared/Services/AccountService.cs +++ b/src/Web/Shared/Services/AccountService.cs @@ -80,18 +80,14 @@ public class AccountService : IDataService, ISupportDataChangedNotifica return (await playerContext.GetAccountsOrderedByLoginNameAsync(offset, count).ConfigureAwait(false)).ToList(); } - var allAccounts = await playerContext.GetAsync().ConfigureAwait(false); - var results = allAccounts.Where(account => - (account.LoginName != null && account.LoginName.Contains(filter, StringComparison.InvariantCultureIgnoreCase)) - || account.Characters.Any(c => c.Name != null && c.Name.Contains(filter, StringComparison.InvariantCultureIgnoreCase)) - ).ToList(); - - if (offset >= results.Count) + var results = (await playerContext.SearchAccountsAsync(filter, offset, count).ConfigureAwait(false)).ToList(); + if (results.Count == 0 && offset > 0) { - return results.Take(count).ToList(); + // The filter narrowed the result set down to less entries than the current page offset - show the first page instead. + results = (await playerContext.SearchAccountsAsync(filter, 0, count).ConfigureAwait(false)).ToList(); } - return results.Skip(offset).Take(count).ToList(); + return results; } catch {