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
{